いつクリはてブロ

いつになったらクリエイティブするの?

〜ゲーム制作者だってテストがしたい!〜DXRubyユーザー向けRspec入門

この記事はDXRuby Advent Calendar 2014 9日目の記事です。
昨日はあおいたくさんの『DXRuby ユーザのための Ruby 組み込みライブラリの紹介』でした。
DXRuby Advent Calendar 2014 - Adventar
あおたくノート — [DXRuby] DXRuby ユーザのための Ruby 組み込みライブラリの紹介
組み込みと聞くと、家電や人工衛星などに入っている小規模コンピュータを連想しますが、そっちでは無かった模様(そりゃそうだ)
Rubyの組み込み機能をよく知ることは、Rubyの黒魔術を学ぶ近道なので、黒魔術を習得したい方にとって昨日の記事はいい入門編になるのではないでしょうか。

メタプログラミングRuby

メタプログラミングRuby

※代表的魔導書

はじめに

Hey, DXRubyユーザーのみんな! 今日もバリバリゲーム作ってるかな?
今回は、普段ゲームを作るのに夢中でテストなんて書かないという方や、そもそもテストって何? という方に向けて、テストの便利さ、安心さについて紹介しようと思います。
テストといっても学力試験のことではありませんのでご安心を。今回あなたはテストを設ける側です。(といってもテスト対象のコードを書くのもおそらくあなた自身ですが)

サンプルコード ジャンプ!

何はともあれ、DXRubyAdventCalendarということで今回はアクションゲームを意識し、こんなサンプルを想定してみました。

  • 地面があってプレイヤーキャラクター(PC)が立っている
  • スペースキーでジャンプする

これをRuby+DXRubyで(何も考えずに)コーディングしてみます。

require 'dxruby'

class PlayerCharacter < Sprite
  attr_accessor :v
  def initialize(x,y,image)
    @v = 0
    super
  end

  def jump
    @v = 20 if(Input.key_push?(K_SPACE))
    @v += -1 # 重力加速度
    self.y += @v
  end
end

image = Image.load("dot.png")
field = Image.new(640,160)
field.box_fill(0,0,640,160,[216,117,35])
pc = PlayerCharacter.new(320,240,image)

Window.loop do
  Window.draw(0,320,field)
  pc.jump
  pc.draw
end

Spriteクラスを継承したPlayerCharacterクラスを定義し、jumpメソッドを追加しました。
スペースキーが押されたときに初速を与え、ジャンプさせます。
また、重力加速度を与えたので、ニュートン力学的に落下します。
このコードを実行してみましょう。
(※注意)実際に動かす場合には、「dot.png」という32*32の画像ファイルを同じディレクトリに置いてください。



キャラがロケットのように打ち上げられたのが確認できたでしょうか?
少し考えれば分かることですが、DXRubyの座標系は下向きがプラスです。
なので重力加速度が画面上向きにかかり、「上に落下」したというわけです。
……このように何も考えずにコーディングすると、見るも無残な結果になります。
もっとも、こういうところからスタートして、ひとつひとつ制作を進めていくのがゲーム制作の醍醐味とも言えますが。

さあ、気を取り直してコーディングを進めましょう。

require 'dxruby'
require "./player_character"

image = Image.load("dot.png")
field = Image.new(640,160)
field.box_fill(0,0,640,160,[216,117,35])
pc = PlayerCharacter.new(320,320,image)

Window.loop do
  Window.draw(0,320,field)
  pc.jump
  pc.draw
end

PlayerCharacterクラスは「player_character.rb」として独立させました。

class PlayerCharacter < Sprite
attr_accessor :v,:bottom

  def initialize(x,y,image)
    @v = 0
    @bottom = y + 32
    super
  end

  def jump
    @v = -20 if(Input.key_push?(K_SPACE))
    @bottom += @v
    self.y = @bottom - 32
    @v += 1 # 重力加速度
  end
end

重力とジャンプ時の初速を正しい方向に修正しました。
また、ちょっとした小細工として、PlayerCharacterクラスに新しくbottom変数を追加し、キャラの画像の底を現在のy座標として扱うようにしました。
実行してみましょう。



今度は起動直後にキャラが地面をすり抜けて落ちていきました。
これは地面で止まるような処理を書いていないからですね。
改良したjumpメソッドがこちら。

  def jump
    @v = -20 if(Input.key_push?(K_SPACE))
    @bottom += @v
    if(@bottom >= 321)
      @bottom = 320
      @v = 0
    end
    self.y = @bottom - 32
    @v += 1 # 重力加速度
  end

果しなき流れの果に

このように、ゲーム制作は山あり谷ありです。
コードを書いて実行して挙動を確かめ、エディタに戻りコードを書く、という開発スタイルの方も結構多いのではないでしょうか。(私もそうです。)
これがサンプルのように簡単なものだったら良いのですが、RPGシミュレーションゲームなど、込み入った複雑なものになると、段々と実際に実行しながらの動作確認はしんどくなってきます。
これをスムーズに行うには、この工程を自動化する必要があります。
それを行う仕組みが、プログラミングにおける「テスト」と呼ばれるものです(ようやく本題)。

Rspecの導入

Rubyのコードをテストするには、Rspecというgemが便利です。

gem install rspec

インストールが完了したら、作業中のディレクトリで次のコマンドを実行しましょう。

rspec --init
  create   spec/spec_helper.rb
  create   .rspec

specディレクトリといくつかのファイルが作成されました。これで準備完了です。

テストコードを書く

実際にテストコードを書いてみましょう。
テストコードは、作成されたspecディレクトリ以下に「player_character_spec.rb」という名前で保存します。

require 'spec_helper'
require 'dxruby'
require './player_character'

describe PlayerCharacter do

  before do
    image = Image.load("dot.png")
    @pc = PlayerCharacter.new(200,200,image)
  end

  it "地面にめりこまない" do
    @pc.v =- 20
    200.times do
      @pc.jump
    end
    expect(@pc.bottom).to eq(320)
  end
end
before do ~ end

の中で、テストに使うオブジェクトの準備を行います。
テストのタイトルを

it "タイトル" do

の形式で書き、処理の内容とアサーションを記述します。
アサーションとは、「この通りになってないとテストを失敗させる」という処理です。

expect(@pc.bottom).to eq(320)

これは「@pc.bottomが320でないとテスト失敗」という意味になります。
先ほど追加したコードで、地面より下に行きそうな場合は座標を調整する処理を書いたので、bottomは320(地面のy座標)になっているはずです。

次のコマンドを実行して、実際にテストを行ってみましょう。

rspec spec/player_characyer_spec.rb

f:id:vivit_jc:20141209234751p:plain
こんな感じの結果が出力されれば成功です。

テスト項目を考える

実はもう一つ、普通のアクションゲームのジャンプと比べておかしなところがこのサンプルにはあります。
テスト項目を考える練習として、少し考えてみてください。
(テスト項目の提案について、有名な題材として『マイヤーズの三角形』というものがあります。興味のある方はググってみてください)
このサンプルで他にどんなテストをすべきでしょうか?
実際に実行してみると分かりやすいかもしれません。



正解は人の数だけありそうですが、私が気になったのは「多段ジャンプができてしまう」、すなわちジャンプ中にスペースキーを押すとさらにジャンプできてしまうという点です。
(もっとも、そういうゲームもありますが)
テストコードに「多段ジャンプしないことを確かめるコード」を追加してみましょう。
以下のコードを「it "地面にめりこまない" do」と同じ階層に書き足します。

it "ジャンプ中はジャンプしない" do
  highest = @pc.bottom
  allow(Input).to receive(:key_push?).and_return(true)
  200.times do
    @pc.jump
    highest = @pc.bottom if(highest > @pc.bottom) 
  end
  expect(highest).to eq(110)
end

このように、テストコードを実際に実行するコードより先に書いてしまうこともできます。
その場合、まだ実装していない部分のせいでテストを実行出来なくなっては困るので、書いてない部分が都合の良い値を返してくれるようにする機能を使います。
これはスタブと呼ばれています。

  allow(Input).to receive(:key_push?).and_return(true)

テスト対象のコード内で「Input.key_push?」が呼ばれたとき、とりあえずtrueを返すようにしています。
今回の場合、この部分は未実装というわけではありませんが、自動化されたテストの中でキー入力を受け付けることはできないので、キー入力されたかのように扱うには、trueを返してもらう必要があります。
つまり、これを記述することで60fpsの全てのフレームでスペースキーを押した状態として扱ってくれるということです。

highestは最高到達高度です。
適当なプリントデバッグなどで、一回ジャンプしたときの最高到達高度が110だと分かったので、「1秒に60回スペースキーを押しても着地するまでは追加でジャンプできない」ことを確かめます。

このテストを実行すると、次のようになるはずです。
f:id:vivit_jc:20141210002751p:plain
テストの結果として、コマンド行の直下に緑のドットと赤いFが並んでいます。
また、下の方に

2 examples, 1 failure

という赤い文字があります。
これはテスト2個中1個が通っていないことを示しています。

今回の場合、多段ジャンプが自由にできるせいで、最高到達高度が-3768に達しています。(y座標はマイナスが上向きなので、画面上部をはるかに突き破っています)


実行コードを修正して、このテストを通るようにしましょう。
修正自体は簡単で、地面にいるときだけスペースキーの入力を受け付けるようにするだけです。

@v = -20 if(Input.key_push?(K_SPACE) && @bottom == 320)

修正したらもう一度テストを実行してみます。
f:id:vivit_jc:20141210003303p:plain
大丈夫みたいですね。

テストを書くメリット

先ほどのように、本実装よりも先にテストコードを用意し、それがクリアされるように開発を進めていく手法を「テスト駆動開発(TDD, Test Driven Development)」と呼びます。
テストを書くと次のようなメリットがあります。

テストを先に書くことで、仕様がはっきり見えてくる

サンプルのような実装をいざしようと思ったとき、「とりあえずジャンプする」としか頭にない状態でコードを書き始めると、冒頭のように迷走することが多いです。
しかし、テスト項目を先に作ることを意識すると、「どうなるべきか」「どうならないべきか」というのがはっきりと見えてきます。
テストのタイトル(it ~ doの内容)を先に考えて列挙するだけでも、かなり頭が整理されるでしょう。

「この部分はバグを出さない」ということが判明している安心感

テストコードがあれば、「とりあえずここの部分は正しく動作する」ということが分かります。
これを増やしていけば、何かバグが見つかって原因を探すときに、いちいち全てを細かく見て回る必要がなくなります。
単にテストを書いていない部分について探せばよいのです。

一度分かったバグは二度と潜り込まない

バグを見つけるたびにそれを再現するテストを追加していけば、うっかり同じバグを書いてしまったときにもそのテストに引っかかるので、そのまま見逃すのを防ぐことができます。

既に書いた部分を壊した場合にもすぐ分かる

なんとなくある変数の名前を変えて、それが思ったより広い範囲に影響を及ぼしてしまい、全て見つけ出すのに苦労したという経験は誰にでもあると思います。
テストが用意してあれば、コードの変更によってどこかが壊れてしまった場合にもすぐに見つかります。

まとめ

当たり前ですが、本コードだけと比べると、テストコードを書いた場合はより多くの時間と手間が掛かります。
しかし、ゲームが複雑になればなるほど、バグが出たときに再現したり修正するのが困難になっていきますから、時間と手間をテストにつぎ込む意義は大いにあると言っていいに違いありません。
また、多人数で開発する場合は、より一層テストの重要性が増していきます。
皆さんもこの機会に是非テストを書いてみてください。

参考

Ruby - はじめてのRSpec - まずテスト書いてからコード書くシンプルなチュートリアル - Qiita
マイヤーズの三角形問題 - 倖せの迷う森


明日はみれいゆーさんの『DXRubyで成長曲線と父母遺伝の再現、描写』です。
なんだかスゴそう……。楽しみです!