RubyMotion + Joyboxでゲームを作る

  • 19
    Like
  • 0
    Comment
More than 1 year has passed since last update.

yonekawaです。
この記事ではJoyboxというライブラリを使用しRubyMotionでゲームを作る方法を紹介します。
JoyboxはiOSでメジャーなゲームエンジンであるCocos2DBox2Dという物理エンジンを内包し、RubyMotionで扱えるようにAPIをラップしたゲーム開発ライブラリです。

Joyboxのインストール/プロジェクトの作成

JoyboxはRubyGems経由でインストールします。

% gem install joybox

JoyboxはRubyMotion Advent Calendarの6日目の記事で紹介された、テンプレートに対応しています。
今回はiOS用のテンプレートを使ってプロジェクトを作成します。

% motion create --template=joybox-ios amazing-game
  Create amaizing-game
  Create amaizing-game/.gitignore
  Create amaizing-game/app/app_delegate.rb
  Create amaizing-game/Rakefile
  Create amaizing-game/resources/Default-568h@2x.png
  Create amaizing-game/resources/fps_images-hd.png
  Create amaizing-game/resources/fps_images-ipadhd.png
  Create amaizing-game/resources/fps_images.png
  Create amaizing-game/spec/main_spec.rb

シーンを作る

まずはSceneに表示するLayerを作成します。Joybox::Core::Layerの子クラスです。
Spriteを作ってキャラクターの画像を表示させています。

class MainLayer < Joybox::Core::Layer
  def on_enter
    character = Sprite.new(
      file_name: 'sprites/character.png',
      position: Screen.center
    )
    self << character
  end
end

MainLayerをSceneに追加します。SceneはJoybox::Core::Sceneの子クラスとして作成します。

class MainScene < Joybox::Core::Scene
  def on_enter
    main_layer = MainLayer.new
    self << main_layer
  end
end

作成したSceneを画面に表示します。
app/app_delegate.rbの最後のtrueを返す前に記述します。

  @window = UIWindow.alloc.initWithFrame(UIScreen.mainScreen.bounds)
  @window.setRootViewController(@navigation_controller)
  @window.makeKeyAndVisible

  # Here
  @director << MainScene.new

  true
end

rakeで実行すれば、Layerに追加したキャラクター画像が画面中央に表示されるはずです。Joyboxではこのようにdirectorに作成したSceneをpushしていくことでゲーム画面を作成していきます。

アニメーション

クールなゲームではアニメーションの存在が欠かせません。
JoyboxではCocos2D由来の豊富なアニメーションAPIが用意されています。
先ほどのキャラクターをアニメーションさせてみましょう。

Move

キャラクターが今いる位置から2秒かけて右へ300移動します

move_by_action = Move.by(position: [300, 0], duration: 2.0)
ease_exponential_in_action = Ease.exponential_in(action: move_by_action)
character.run_action(ease_exponential_in_action)

Rotate

キャラクターが3秒かけてかけて360度回転します

rotate_by_action = Rotate.by(angle: 360.0, duration: 3.0)
character.run_action(rotate_by_action)

Blink

キャラクターが点滅します

blink_action = Blink.with(times: 10, duration: 10.0)
character.run_action(blink_action)

複数のアニメーション

複数のアニメーションをまとめて実行することもできます。
例えば順番に実行したい場合はSequenceを使います。

sequence_action = Sequence.with(actions:[ease_exponential_in_action, rotate_by_action])
character.run_action(sequence_action)

同時に実行したい場合にはSpawnを使います。

spawn_action = Spawn.with(actions:[ease_exponential_in_action, rotate_by_action])
character.run_action(spawn_action)

衝突判定

例えばシューティングゲームなどで、弾が自機や敵機に当たったかどうかを判定したいことがあります。いくつか方法はありますが、今回はCGRectIntersectsRectを使った方法を紹介します。

アニメーション等で常に動いているSprite同士の衝突判定をするには、ゲーム内で更新が発生する度にSpriteのboxが重なっていることを判定するのが簡単です。
そのためにJoyboxにはshedule_updateというメソッドが用意されています。

class MainLayer < Joybox::Core::Layer
  def on_enter
    @character = Sprite.new(
      file_name: 'sprites/character.png',
      position: Screen.center,
      alive: true
    )

    move_by_action = Move.by(position: [300, 0], duration: 2.0)
    ease_exponential_in_action = Ease.exponential_in(action: move_by_action)
    character.run_action(ease_exponential_in_action)

    self << @character

    @enemy = Sprite.new(
      file_name: 'sprites/enemy.png',
      position: [Screen.width - 100, Screen.half_height].to_point
    )

    self << @enemy

    schedule_update do |dt|
      check_for_collisions if @character[:alive]
    end
  end

  def check_for_collisions
    if CGRectIntersectsRect(@character.bounding_box, @enemy.bounding_box)
      @character.stop_all_actions
      blink_action = Blink.with(times: 10, duration: 10.0)
      @character.run_action(blink_action)
      @character[:alive] = false
    end
  end

schedule_updateに渡されたブロックが、Cocos2Dのゲームループ毎に毎回呼び出されます。
その度にCGRectIntersectsRectでキャラクターと敵のbouding_boxを判定することで、衝突判定を実現できます。
あとは今回のようにキャラクターのすべてのアクションを止めて点滅させる、みたいなことをすれば敵に当たって死んだ、みたいな表現を実現できます。

物理エンジン

本来はRubyMotionではC++のライブラリが使えないため、Box2Dは使えないのですが、JoyboxではBox2DをObjective-Cでラップして使えるようにしています。

class PhysicsLayer < Joybox::Core::Layer
  scene

  def on_enter
    init_world
    create_character

    schedule_update do |delta|
      @world.step(delta: delta)
    end
  end

  def init_world
    @world = World.new(gravity: [0, -10])
    body = @world.new_body(position: [0, 0]) do
      edge_fixture start_point: [0, 0],
      end_point:   [Screen.width, 0]
      edge_fixture start_point: [0, Screen.height],
      end_point:   [Screen.width, Screen.height]
      edge_fixture start_point: [0, Screen.height],
      end_point:   [0, 0]
      edge_fixture start_point: [Screen.width, Screen.height],
      end_point:   [Screen.width, 0]
    end
  end

  def create_character
    @character_body = @world.new_body(
      position: Screen.center,
      type: KDynamicBodyType
    ) do
      polygon_fixture(
        box: [47, 48.5],
        friction: 0.7,
        density: 1.0
      )
    end

    @character = PhysicsSprite.new(file_name: 'character.png', body: @character_body)
    self << @character
  end
end

まずinit_worldで、Box2Dの物理計算のためのWorldを作ります。
そしてcreate_characterで先ほど同様キャラクターを追加するのですが、この時にworldを経由してbodyを作り、PhyisicsSpriteを作ります。これによりSpriteを物理演算の対象にできます。
そしてゲームループで、worldのstepを進めることで、物理演算が実行されるようになります。

例えばこういうコードをon_enterに入れると、画面をタッチするとキャラクターが吹っ飛びます。apply_forceで物理的に力が加えられるからです。

on_touches_ended do |touches, event|
  @character.body.apply_force(force: [1000, 1000])
end

SpriteKit

ここまでJoyboxの紹介をしてきましたが、iOSにはSpriteKitというフレームワークがあります。
SpriteKitはiOS7からSDKに追加された、Apple製の2Dゲームフレームワークです。物理エンジンも搭載しており、Cocos2Dとほぼ同じことが実現できると言われています。
Cocos2Dの作者もSpriteKitに言及しています。

RubyMotionならSpriteKitもすぐ使えるということで、比較のために試しに書いてみました。
簡単なサンプルですがこちらにあげてあります。

class MainScene < SKScene
  def initWithSize(size)
    super(size)
    self.physicsBody = SKPhysicsBody.bodyWithEdgeLoopFromRect(CGRectMake(0, 0, self.size.width, self.size.height))
    self
  end

  def createCharacter(location)
    character = SKSpriteNode.alloc.initWithImageNamed('character')
    character.name = "character"
    character.physicsBody = SKPhysicsBody.bodyWithRectangleOfSize(character.size)
    character.physicsBody.dynamic = true
    character.position = [location.x, location.y]
    character
  end

  def touchesBegan(touches, withEvent: event)
    touch = touches.anyObject
    location = touch.locationInView(touch.view)

    character = createCharacter(location)
    self.addChild(character)
  end
end

感想としては、UIKitとの親和性が高くシームレスに連携されるので通常のアプリ開発と同じような流れでゲームUIが作れるのはいいなと思いました。
Joybox(Cocos2D)のようにゲーム開発フレームワークという感じではなくSprite操作を楽にできるようにする機能群という印象を受けました。
ゲーム開発の取っ掛かりとしてAPIもシンプルだしよさそうです。

Joybox vs SpriteKit

RubyMotionでゲーム開発をする場合、JoyboxとSpriteKitのどちらを選ぶべきでしょうか。これは難しい問題です。

SpriteKitに関してはやはりApple純正というのは強く、今後もXcodeなどデフォルトのツールなどのサポートが充実していくことが期待できます。パフォーマンスなども最適化されていると予想されます。
これから勉強していくならSpriteKitは有力だと思います。

Cocos2Dには今まで多くのゲームで採用されてきた実績があり、情報も多く枯れたゲームエンジンと言えます。Tile Mapなど機能も充実してますし、ゲーム開発をする上で何かが足りなくて困ることは少ないです。
そしてJoyboxにはRuby likeなAPIでコードがスッキリ書けたりテストが書きやすかったりとRubyMotionならではのメリットがあります。

オープンソースなので何か困ったら開発者に直してもらうこともできますし、自分でソースを見て直すこともできます。実際僕も自分で使うのに必要な機能を自分で追加してPull Requestしたところ、すぐ取り込んでもらえました。
この辺りをメリットとして感じるのであればJoyboxはとてもいいライブラリだと思っています。

もちろんiOS6への対応が求められるのであればJoybox一択ですね。

RubyMotion + games = happiness

JoyboxのJuan Jose Karamも言っていますが、RubyMotionでゲームを開発するのはすごく楽しい体験です。使い慣れた道具でカジュアルにゲームを作れる環境はなかなかありません。
Joyboxを選ぶにせよSpriteKitにせよ、ぜひ一度体験してみて楽しいゲームを生み出してほしいと思います。

Joyboxは引き続き開発が進められているので、何か問題があればIssueやPull Requestを送ってみるといいのではないでしょうか。

Links

Joybox

SpriteKit

Sample Code