Vimでゲームを作るためのtips

なんかvimでゲームを作るのがブームのようなので(w,いくつかvimscriptでゲームを作ってみて分かったことをまとめようと思います.


最初に言っておくと,これはゲーム作成に限りませんが,vimscriptを書くコツはいかに他のvimscriptから似たような処理を見つけて抜き出してくるかだと思います.
Redirecting…
にいくつか紹介されているので,それのソースを見るのが一番速いと思います^^;



・バッファ作成
もしバッファが作成されていなければ画面を分割して新たにバッファを作り,バッファがあればそのバッファに移動します.(この例では ==MineSweeper== )

let winnum = bufwinnr(bufnr('==MineSweeper=='))
if winnum != -1
    if winnum != bufwinnr('%')
        exe "normal \<c-w>".winnum."w"
    endif
else
    exec 'silent split ==MineSweepe==
endif

現在のウィンドウをそのまま分割せず利用するのであれば以下の記述で十分です.

edit `='==SPACE INVADER=='`


・バッファのクリア

silent %d _


・画面描写(バッファの書き換え)
setline()を使います.setline()の第二引数に文字列のリストを渡すことで複数行を一気に書き換えることができます.ある2次元の配列をフィールドとして確保するというのがいいと思います.ただし,文字列はインデックスでアクセスできても値を変更することができないことに注意が必要です.

let s:field = ["#######",
              \"#     #",
              \"#     #",
              \"#######",]

function! s:update()
    call setline(1,s:field)
endfunction

この例でs:fieldを書き換えたい場合は以下のようにして処理できます.

funciton s:change(x,y,c)
    let line = s:field[a:y]
    let left = (a:x == 0 ? '' : line[: a:x - 1])
    let right = (a:x == len(line) - 1 ? '' : line[a:x + 1 :])
    let s:field[a:y] = left . a:c . right
endfunction

また,s:field[i]をcharの配列にして,setline()するときにjoin()することもできます.この場合変更は s:field[y][x] = c のようにできますが,前のと比べて描画は遅くなると思います.

let s:field = [['#','#','#','#','#','#','#'],
              \['#',' ',' ',' ',' ',' ','#'],
              \['#',' ',' ',' ',' ',' ','#'],
              \['#','#','#','#','#','#','#'],]
function! s:update()
    for i in range(len(s:field))
        let str = join(s:field[i],'')
        call setline(i+1,str)
    endfor
endfunction

また,バッファの特定の場所を変更する処理は以下の処理でできます(GitHub - mattn/invader-vim: invader game in vimより)

" y行目x列目の文字をcに変更する
function! s:update(x, y, c)
  let s = getline(a:y)              
  let o = ''
  if a:x > 0                        
    let o .= s[:a:x-1]              
  elseif a:x < 0
    let o .= a:c[-a:x :]
  endif
  let o .= a:c
  let o .= s[a:x+(len(a:c)+1)-1:]
  call setline(a:y, o)
endfunction


・10fpsで動作
sleep を使います.

while s:loop == 1
    (何かする…)
    call s:update()
    sleep 100m
    redraw
endwhile

また,autocmdを使って疑似的に定期的な処理をおこなうことができます.autocmdを使って疑似タイマーを作る方法はhttp://vim-users.jp/2010/09/hack173/を参照してください.また,以下の例も参考になると思います.(GitHub - tyru/pacman.vim: *incomplete yet* *patches welcome*より)

function! s:create_buffer()
    ...
    " Global options.
    let b:pacman.save_updatetime = &updatetime
    set updatetime=100
    let b:pacman.save_lazyredraw = &lazyredraw
    set lazyredraw
    let b:pacman.save_virtualedit = &virtualedit
    set virtualedit=
    let b:pacman.save_insertmode = &insertmode
    set noinsertmode
    ...
    augroup pacman
        autocmd!
        call s:register_polling_autocmd()
        " Inhibit insert-mode.
        autocmd InsertEnter <buffer> stopinsert
        " Pause on BufLeave, BufEnter.
        autocmd BufLeave <buffer> call s:pause()
        autocmd BufEnter <buffer> call s:restart()
        " Clean up all thingies about pacman.
        autocmd BufDelete <buffer> call s:clean_up()
    augroup END
    ...
endfunction

function! s:register_polling_autocmd()
    autocmd pacman CursorHold <buffer> silent call feedkeys("g\<Esc>", "n")
    autocmd pacman CursorHold <buffer> call s:main_loop()
    let b:pacman.pausing = 0
endfunction

function! s:clean_up()
    ...
    "設定を元に戻す
    let &updatetime = b:pacman.save_updatetime
    let b:pacman.save_updatetime = -1
    let &lazyredraw = b:pacman.save_lazyredraw
    let b:pacman.save_lazyredraw = -1
    let &virtualedit = b:pacman.save_virtualedit
    let b:pacman.save_virtualedit = ''
    let &insertmode = b:pacman.save_insertmode
    let b:pacman.save_insertmode = ''
    ...
endfunction


・キー入力
マッピングを使うのが素直なやり方だと思います.スクリプトローカル関数をマッピングする場合には<SID>を使う必要があることに注意してください.

nnoremap <silent> <buffer> x            :call <SID>_click()<CR>

マッピングを使う注意点として,例えば z をマッピングした場合,標準的なvimでは zz が行を中央に持ってくる動作にマッピングされているので,zを押してもすぐにzのマッピングが動作せず,timeoutlen(標準で1000ms)待ってからzのキーが反応します.なるべく被らないマッピングを選ぶのがいいです.ノーマルモードの場合おそらく x をマッピングして変更している人はいないと思うので,何かのアクションにはxを使うといいと思います.
もう一つの方法として,getchar()を使う方法があります.特にgetchar(0)を使うと1文字読み込めるときだけ読み込むので,sleepを使って動作させている場合はこの方法がいいと思います.

  let c = nr2char(getchar(0))
  if c == 'x'
      (何か処理)
  else if ...

  endif
  (何か処理)
  sleep 100m
  ...


・設定しとくといいオプション

setl buftype=nowrite     "バッファの内容を保存しない
setl noswapfile          "スワップファイルを作成しない
setl bufhidden=wipe      "バッファがウィンドウ内から表示されなくなったら削除
setl nonumber            "行番号を表示しない
setl nowrap              "ラップしない
setl nocursorline        "カーソル行をハイライトしない
setl nocursorcolumn      "カーソル列をハイライトしない


・乱数

http://vim-users.jp/2009/11/hack98/ で紹介されている方法を使います.万が一reltime()が使えない場合には連番を返すようにしています.

let s:rand_num = 1
function! s:rand()
    if has('reltime')
        let match_end = matchend(reltimestr(reltime()), '\d\+\.') + 1
        return reltimestr(reltime())[match_end:]
    else
        let s:rand_num += 1
        return s:rand_num
    endif
endfunction


追記
あまりこの乱数は良いとは言えないので, ynkdirさんによる msvc で使われている乱数の実装を使った方が良いみたいです.

・メッセージをハイライトして表示

function! s:message(msg)
    echohl WarningMsg
    echo a:msg
    echohl None
endfunction


追記するかも?


是非皆さんも作ってみてくださいw