2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

NimでSDL2 ③ 入力処理

Last updated at Posted at 2020-12-05

はじめに

NimでSDL2 ① 準備とウィンドウ表示
NimでSDL2 ② 画像を表示

前回画像を表示したので、今回はキーボードから入力を処理して画像を移動してみる。

概要

  1. キーボードイベントを処理して入力されたキーを取得する
  2. 入力されたキーに応じて画像の出力座標を更新して出力する

リファクタリング

前回まででちとコードがわちゃわちゃしてきたのでいったん整理。
詳細はコメントを参照してくださいー。

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秒に短縮。

結果

keyboard.gif
※ gifアニメです。

次は?

今まで適当にWaitしてたので次回はFPS(Frame Per Second)を調整して行きたいですね。

以上〜。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?