はじめに
NimでSDL2 ① 準備とウィンドウ表示
NimでSDL2 ② 画像を表示
前回画像を表示したので、今回はキーボードから入力を処理して画像を移動してみる。
概要
- キーボードイベントを処理して入力されたキーを取得する
- 入力されたキーに応じて画像の出力座標を更新して出力する
リファクタリング
前回まででちとコードがわちゃわちゃしてきたのでいったん整理。
詳細はコメントを参照してくださいー。
import sdl2, sdl2/image, os, sugar
# エラー処理のテンプレート
template sdlFailIf(cond: typed, desc: string) =
if cond: raise Exception.newException(
desc & ": " & $getError())
# SDL2を管理するオブジェクト
type SDL2Context = ref object
window: WindowPtr
renderer: RendererPtr
cleanups: seq[proc ()]
# クリーンアップ処理。登録されたクリーンアップ関数を実行する
proc cleanup(self: SDL2Context) =
# 追加されたcleanup関数を逆順で実行
while self.cleanups.len > 0:
(self.cleanups.pop)()
# 各種初期化
proc init(self: SDL2Context, title: string, w, h: cint): SDL2Context {.discardable.} = # .discardableは戻り値無視可能関数
# 自身を返す
result = self
# 画像を扱うのでINIT_VIDEOフラグを立てて初期化
sdlFailIf(not sdl2.init(INIT_VIDEO), "SDL2 init")
# SDL2のクリーンアップ処理を登録
self.cleanups.add(() => sdl2.quit())
# Window作成
self.window = createWindow(title = title,
x = SDL_WINDOWPOS_CENTERED,
y = SDL_WINDOWPOS_CENTERED,
w = w, h = h,
flags = SDL_WINDOW_SHOWN)
sdlFailIf(self.window.isNil, "createWindow")
# windownのクリーンアップ処理を登録
self.cleanups.add(() => self.window.destroy)
# image初期化
sdlFailIf(image.init(IMG_INIT_PNG) != IMG_INIT_PNG, "image init")
# imageクリーンアップ処理
self.cleanups.add(() => image.quit())
# windowからrenderer作成
self.renderer = self.window.createRenderer(
index = -1,
flags = Renderer_Accelerated or Renderer_PresentVsync)
# renderer作成エラーチェック
sdlFailIf(self.renderer.isNil, "createRenderer")
# rendererクリーンアップ
self.cleanups.add(() => self.renderer.destroy)
# 背景色を設定
self.renderer.setDrawColor(r=0, g=128, b=128)
# テクスチャー読み込み
proc loadTexture(self: SDL2Context, path: string): TexturePtr =
# rendererを使って画像を読み込んでテクスチャにする
result = self.renderer.loadTexture(path)
# 画像読み込みエラーチェック
sdlFailIf(result.isNil, "loadTexture")
# イベント処理
proc pollEvent(self: SDL2Context): bool =
# 戻り値をtrueで初期化
result = true
# イベント取得用変数
var event = defaultEvent
# 発生している全てのイベントを処理
while pollEvent(event):
# イベントの種類に応じて処理
case event.kind:
# 終了イベントがきたらfalseを返して終了
of QuitEvent: return false
# その他は無視
else: discard
# メインループ
proc mainLoop(self: SDL2Context) =
# テクスチャー読み込み
let neko = self.loadTexture("genbaneko.png")
# 描画するサイズ
let
w: cint = 256 # 描画する画像の幅
h: cint = 256 # 描画する画像の高さ
# Windowサイズを取得用変数
var
windowWidth: cint
windowHeight: cint
# windowサイズ取得
self.window.getSize(windowWidth, windowHeight)
# 描画先のRectを作成(Windowの中心に表示)
var dst = rect(windowWidth div 2 - w div 2, windowHeight div 2 - h div 2, w, h)
# 画面初期化
self.renderer.clear
# レンダリング(描画元はnilを指定しているので画像全体を描画)
self.renderer.copyEx(neko, nil, dst.addr, 0.0, nil, SDL_FLIP_NONE)
# レンダリング結果を画面に反映
self.renderer.present
# イベントループ
while self.pollEvent:
# CPUを使いきらないように0.1秒Wait
sleep(100)
# メイン関数
proc main() =
# SDL2管理オブジェクトを作成
let sdl2 = SDL2Context()
# 例外が発生してもクリーンアップ処理を実行するようにdefer定義
defer: sdl2.cleanup
# 初期化してメインループを実行
sdl2.init("SDL2 Image Sample", 640, 480).mainLoop
# メイン関数実行
main()
概ね変数をSDL2Contextオブジェクトにまとめて
各処理を関数かしてるだけだけど、クリーンアップ処理はちょっと工夫して
関数の配列(cleanups)を用意して、初期化したらそこにクリーンアップ関数を追加しておいて
しかるべき時にその配列の関数を呼ぶ、っていう方法にしてすっきりさせております。
それとアロー演算子で無名関数を定義できるよう、sugarをimportしております。
画像移動処理追加コード
import sdl2, sdl2/image, os, sugar, sets
template sdlFailIf(cond: typed, desc: string) =
if cond: raise Exception.newException(
desc & ": " & $getError())
type SDL2Context = ref object
window: WindowPtr
renderer: RendererPtr
cleanups: seq[proc ()]
pressed: HashSet[cint] # 押されているキーを格納するハッシュセット
proc cleanup(self: SDL2Context) =
while self.cleanups.len > 0:
(self.cleanups.pop)()
proc init(self: SDL2Context, title: string, w, h: cint): SDL2Context {.discardable.} =
result = self
sdlFailIf(not sdl2.init(INIT_VIDEO), "SDL2 init")
self.cleanups.add(() => sdl2.quit())
self.window = createWindow(title = title,
x = SDL_WINDOWPOS_CENTERED,
y = SDL_WINDOWPOS_CENTERED,
w = w, h = h,
flags = SDL_WINDOW_SHOWN)
sdlFailIf(self.window.isNil, "createWindow")
self.cleanups.add(() => self.window.destroy)
sdlFailIf(image.init(IMG_INIT_PNG) != IMG_INIT_PNG, "image init")
self.cleanups.add(() => image.quit())
self.renderer = self.window.createRenderer(
index = -1,
flags = Renderer_Accelerated or Renderer_PresentVsync)
sdlFailIf(self.renderer.isNil, "createRenderer")
self.cleanups.add(() => self.renderer.destroy)
self.renderer.setDrawColor(r=0, g=128, b=128)
# キー状態を初期化
self.pressed = initHashSet[cint]()
proc loadTexture(self: SDL2Context, path: string): TexturePtr =
result = self.renderer.loadTexture(path)
sdlFailIf(result.isNil, "loadTexture")
# キー状態を更新
proc keyStateUpdate*(self: SDL2Context, event: KeyboardEventPtr) =
case event.kind
# キーが押されたらそのキーを追加
of KeyDown: self.pressed.incl(event.keysym.sym)
# キーが話されたらそのキーを削除
of KeyUp: self.pressed.excl(event.keysym.sym)
else: discard
proc pollEvent(self: SDL2Context): bool =
result = true
var event = defaultEvent
while pollEvent(event):
case event.kind:
of QuitEvent: return false
# キーイベントならキー状態を更新
of KeyDown, KeyUp: self.keyStateUpdate(event.key)
else: discard
proc mainLoop(self: SDL2Context) =
let neko = self.loadTexture("genbaneko.png")
let
w: cint = 256
h: cint = 256
speed: cint = 3 # 移動スピード
var
windowWidth: cint
windowHeight: cint
self.window.getSize(windowWidth, windowHeight)
var dst = rect(windowWidth div 2 - w div 2, windowHeight div 2 - h div 2, w, h)
while self.pollEvent:
# キーの状態に応じて、画像出力座標を変更
if K_RIGHT in self.pressed: dst.x += speed
if K_LEFT in self.pressed: dst.x -= speed
if K_DOWN in self.pressed: dst.y += speed
if K_UP in self.pressed: dst.y -= speed
# 描画処理をイベントループ内に移動
self.renderer.clear
self.renderer.copyEx(neko, nil, dst.addr, 0.0, nil, SDL_FLIP_NONE)
self.renderer.present
# 滑らかに動かすためにWait時間を調整
sleep(16)
proc main() =
let sdl2 = SDL2Context()
defer: sdl2.cleanup
sdl2.init("SDL2 Keyboard sample", 640, 480).mainLoop
main()
追加箇所はコメント記載部分。
キーのHashSetを用意しておいて
イベント処理でキーイベントが発生したら、
押されたら追加、離されたら削除してキー状態を保持、
キーが押されていたら出力先座標を更新して描画するだけ。
キーの定義はnimのSDL2ラッパーのここに定義されてます。
フレーム毎の待ち時間が0.1秒だと動きがガタガタだったので
0.016秒に短縮。
結果
次は?
今まで適当にWaitしてたので次回はFPS(Frame Per Second)を調整して行きたいですね。
以上〜。