Edited at
RubyDay 18

CRuby でゲームを作って iPhone で動かそう

More than 3 years have passed since last update.

この記事は Ruby Advent Calendar 2015 の 18日目の記事です。

実は去年の Ruby Advent Calendar にも記事を書いており、こちらはその続きの内容となります。

去年は CRuby で動く Mac OS X 用の GUI ツールキットを使って、ブロック崩しゲームを30行程で作りました。今年はその GUI ツールキットを iOS の UIKit に移植したので、作ったゲームを iPhone 上でも動かしてしまおうというお話です。


はじめに

去年作ったブロック崩しゲームを iPhone で動かしてもいいのですが、折角ですからまた別のゲームを作りましょう。横スクロールして障害物や敵をジャンプで避け、コインを集めていくゲームにしたいと思います。


Mac 上での開発環境の準備

まずは今回も Mac 上でゲームを仕上げます。

Ruby のスクリプトを書き、Mac 標準搭載の ruby 処理系で OSX のアプリとして動かします。そしてその後 Xcode 上で iPhone 用のプロジェクトを作成し、スクリプトをそちらにコピーすることで iPhone 上で動作させてみたいと思います。

という事で、まず OS X 上での環境を用意しましょう。


Gem でインストール

GUI ライブラリ自体は Gem としても公開していますので導入は簡単・・・と言いたい所ですが、Boost に依存していたりするのでネイティブ実装部分のビルドが素直には行かないかもしれません。。。

(もしビルドで Boost 関係のヘッダが見つからないようなエラーになる場合は、とりあえずビルドを通したいだけなら一旦 /usr/include/boost の辺りにヘッダを置いておけばコンパイルが通る様になると思います)

$ gem install reflexion

うまく行けばこのコマンドでインストールが完了します。


GitHub から clone してインストール

もしくは GitHub で最新を取ってくる手も有ります。

$ git clone https://github.com/xord/reflexion.git

$ cd reflexion
$ rake all install


インストールの確認

インストールが無事に済み、

$ ruby -e 'p require "reflex"'

として 'true' と表示されれば準備完了です。

もし興味があれば reflex/samples 以下のサンプルファイルを実行してみても面白いかもしれません。


簡単なジャンプアクションゲームを作る

では早速ゲームを作り始めてみましょう。

まずは OS X のアプリなのでウィンドウの設定から始めます。

setup do

size 800, 400 # ウィンドウサイズの調整
flow :none # (おまじない)
gravity 0, 9.8 * meter # 画面下方向の重力

root.wall.clear_fixtures # 標準でウィンドウの枠として壁が作られてしまうので除去
add_ground # 地面要素の追加
add_bricks 50 # 障害物を 50 個追加
add_coins 50 # スコアを稼ぐ用のコインを 50 個追加
add_enemies 10 # 敵キャラを 10 体追加
add_player # プレイヤーキャラクタの追加
end

最終的に iPhone の横画面用に作るため、適当にウィンドウサイズは横長に設定します。そしてマリオ風のジャンプアクションという事で重力を設定し、その後は画面の構成要素を追加していくメソッドを順番に呼んでいます。

次に要素追加のメソッドを個別に見ていきます。まずは一番シンプルな障害物の追加から。各行の意味はコメントをご覧ください。

def add_bricks (count)

count.times do
window.add RectShape.new { # 矩形の障害物をウィンドウに追加
pos rand(range), 200 # X の位置はランダム、Y の位置は上から 200 ピクセルの位置
size *(0..1).map {rand(20..100)} # 縦・横の大きさはそれぞれ 20〜100 のランダム
fill rand, rand, rand # 色も RGB それぞれランダム
dynamic true # 物理演算の対象であり固定されない形状
density 1 # 密度指定をしないと形状が回転しない
}
end
end

次にプレイヤーがスコアを稼ぐためのコインの追加です。コインなので形は丸です。障害物と違って他形状との衝突時の処理を追加しています。

def add_coins (count)

count.times do
window.add EllipseShape.new { # 丸のコインオブジェクトをウィンドウに追加
pos rand(range), 100 # X の位置はランダム、Y の位置は障害物よりも上となるように 100 ピクセルの位置
size 30 # 縦横は 30 で固定
fill :yellow # コインらしく黄色に
static true # 物理演算の対象にするが、コイン自体は位置固定のため static 指定
sensor true # 幽霊のように他形状と重なっても衝突しない
on :contact do # 他の形状と接触したときの処理
remove_self # コイン自体を削除
$score += 1 # スコアに +1
end
}
end
end

次はゲームらしくするための敵の追加です。設定は障害物、コインと大きくは違いませんが、プレイヤーが接触してしまった時はゲームオーバーとしました。

def add_enemies (count)

count.times do
window.add RectShape.new { # 矩形をウィンドウに追加
pos rand(range), 200 # X の位置はランダム、Y の位置はコインの少し下になるように
size 50 # 縦横の大きさは 50 固定
fill :red # 敵は危険なので赤に
static true # 衝突の処理はしたいので物理演算対象にするが位置は動かさないので static 指定
sensor true # 衝突の検出のみで物体同士で反発はしない
on :contact do |e| # 他の物体と接触した時の処理
$gameover = true if e.view == $player
# 接触相手がプレイヤーだったらゲームオーバーフラグを立てる
end
}
end
end

形状をいろいろ配置しても、重力があるため落下して画面外にまで出てしまいます。なので次は地面の追加です。

位置固定の線分を追加しているだけなので特に難しい所は無いですが、ただの平らな地面だとつまらないので、Perlin ノイズでデコボコさせています。

def add_ground ()

$ground = window.add View.new { # 衝突判定用の形状の指定と描画は自前でやるので、Shape ではなくただの View を追加
width 10000 # 横スクロールのゲームなので横幅を長く
height parent.height # 縦幅はウィンドウの高さに合わせる
static true # 物理演算の対象に

edges = (0..width).step(5).map do |x| # 0〜10000ピクセルの間を5ピクセル間隔で線分を追加していく
noise = Rays.perlin(x / 100.0, 0) # Perlin ノイズを使ってデコボコに
[x, height + noise * 30 - 50] # noise は -1.0〜1.0 なので 30 倍することで -30.0〜30.0 に
end
edges = [[0, 0]] + edges + [[width, 0]] # 地面だけだと左右の端で落ちてしまうので壁も追加

body.clear_fixtures # デフォルトで生成される形状を削除
body.add_edge *edges # 生成した点の配列を物理演算用の線形状として登録
on :draw do |e| # 地面の描画処理
e.painter.push fill: nil, stroke: :white do # ブロック内は塗り無し・白い線で描画
lines *edges # 線を描画
end
end
}
end

最後にプレイヤーキャラクタの追加です。

def add_player ()

$player = window.add EllipseShape.new { # プレイヤーは丸い形状
@jump_count = 0 # 多段ジャンプの判定用

pos 50, 50 # 初期位置
size 30 # 幅と高さは 30
dynamic true # 重力にしたがって落下するので dynamic 指定
density 1 # 密度
friction 1 # 摩擦

def self.jumpable? () # ジャンプ可能な状態かを返す
@jump_count <= 1 # 2段ジャンプまで
end
def self.jump () # ジャンプする
return unless jumpable? # ジャンプ不可の状況なら何もしない
v = velocity # その時点での速度のベクトルを取得
v.y = -5 * meter # Y 方向の速度を 5 メートルに
velocity v # 上向きの速度を設定することでジャンプさせる
@jump_count += 1 # 多段ジャンプ 1 回消費
end

on :update do # 毎フレームのプレイヤー形状の更新処理
dir = 0
dir -= 1 if $left # $left, $right は Boolean
dir += 1 if $right # ユーザの入力状況に応じて挙動を変える
self.angular_velocity = 360 * 3 * dir
# ユーザの入力に応じて秒速+/-1080度の回転速度を設定
end
on :contact_begin do # 他形状に接触したら?
@jump_count = 0 # ジャンプのカウントをクリア
end
}
end

これで大体の要素は揃いましたので、あとはゲームとしての更新、描画、入力の処理です

update do                                      # 毎フレームごとに呼ばれる処理

old_x = window.root.scroll.x # 現在の横スクロール位置
new_x = $player.center.x - window.width / 2 # プレイヤーの位置
window.root.scroll_to (old_x + new_x) / 2, 0 # 上 2 つの位置を元に、プレイヤーを追いかけるように画面をスクロールさせる
end

draw do # 毎フレームの描画処理
fill :white # 白塗りで
font nil, 30 # 大きさ 30 の標準フォントで
text "SCORE: #{$score}", 10, 10 # スコアを左上に描画
text "#{event.fps.to_i} FPS", 10, 50 # その下に FPS も描画
if $gameover # ゲームオーバーフラグが立ってたら
fill :red # 赤塗りで
font nil, 100 # 大きさ 100 の標準フォントで
text "GAMEOVER!", 100, 100 # ゲームオーバーを表示
end
end

key do # キー入力ごとに呼ばれる処理
next unless down? || up? # キーの押下もしくは押上の時のみ処理したい
$left = down? if left? # 左キーが押下された?
$right = down? if right? # 右キーが押下された?
$player.jump if space? && down? # スペースキーの押下だったらプレイヤーキャラクタをジャンプさせる
end

と、これでゲームのコア部分は一通り完成です。基本的には構成要素の個別の設定が大半で、ゲームとして成り立たせるロジックのコードは最小限になっていますね。


OS X 上で動かす

では Mac 上で動かしてみましょう。

ライブラリが正常にインストールされていれば、動かし方は通常の Ruby スクリプトと同じです。

$ ruby jump_action.rb

こんな画面が表示され、左右のカーソルキーで移動、スペースキーでジャンプすれば成功です。

Kobito.6tlgCo.png


iPhone で動かす

では次に iPhone で動かしてみましょう。


Xcode でプロジェクトを作成する

iPhone で動かすためには、通常のアプリケーションと同じように Xcode のプロジェクトを作成します。

Xcode のメニューから "File > New > Project..." を選んで iOS の "Single View Application" を選択しましょう。プロジェクト名に好きな名前を指定し、言語を Objective-C に。Device は任意ですが iPhone を選びます。

設定が完了してプロジェクトが作成されたら、一旦 Xcode を終了しプロジェクトフォルダーの直下に下のような Podfile を作成します。

platform :ios, '7.0'

pod 'CRuby', git: 'https://github.com/xord/cruby'
pod 'Reflexion', git: 'https://github.com/xord/reflexion'

Podfile が用意出来たら早速 CocoaPods を使って必要なライブラリをプロジェクト内に取り込みます。

$ CRUBY_PLATFORM=ios pod install --verbose

ここで注意です。CRUBY_PLATFORM と --verbose の指定は必ずしてください。それぞれ以下の様な効果がありますので、うっかり "pod install" とだけ実行するとおそらく辛い事になると思います。(私のマシンが Late 2010 の 11インチの MacBook Air なので非力すぎるだけという可能性もありますが・・・)


  • "CRUBY_PLATFORM=ios"

    不必要なアーキテクチャ用のバイナリを生成しないのでビルドの時間を短く出来る。


  • "--verbose"

    ライブラリのビルドにとても時間がかかるため、この指定をしておかないと進捗が全く表示されず正しくインストール処理が継続しているのかが目に見えにくくとても不安になる


(ビルドに時間がかかるのは CRuby の 'configure && make' を必要な CPU のアーキテクチャ数分やってるからです。OS X 用の i386、x86_64、iPhone シミュレータ用の i386、x86_64、iPhone 用の armv6、armv7、armv7s、arm64 と、全部指定したら8回も libruby-static.a をビルドするので大変です><

今だと、iPhone6 の実機だけで動作確認ができればいいなどであれば、追加で "CRUBY_ARCHS=arm64" の指定をすればビルドも1回で済むようにはなってますので、普段の開発時にはこれだけでもいいのかもしれません。)


プロジェクトの設定

CRuby と Reflexion のライブラリが上手くプロジェクトに取り込めたら、次はプロジェクトの細かい設定をしていきます。(といってもそんなに沢山はありませんが。)

まずは CocoaPods を普段から使われている方はご存知でしょうが、.xcodeproj ではなく .xcworkspace を開きます。そうすると取り込んだライブラリが Pods というプロジェクトにまとまって読み込まれていると思います。

$ open *.xcworkspace

としてワークスペースを開きましょう。そしてまずはビルドしてみますとエラーが出ますね。

ld: 'Pods/CRuby/build/CRuby_ios.framework/CRuby_ios(bigdecimal.o)' does not contain bitcode. You must rebuild it with bitcode enabled (Xcode setting ENABLE_BITCODE), obtain an updated library from the vendor, or disable bitcode for this target. for architecture arm64

clang: error: linker command failed with exit code 1 (use -v to see invocation)

ビットコード周りですね。無効にしましょう。プロジェクトの "Build Settings" に "Enable Bitcode" というのがあってデフォルトで "Yes" となっているので "No" に変えます。

そしてもう一度ビルド。

duplicate symbol _OBJC_CLASS_$_AppDelegate in:

Build/Intermediates/Demo.build/Debug-iphoneos/Demo.build/Objects-normal/arm64/AppDelegate.o
Build/Products/Debug-iphoneos/libPods-Reflexion.a(app_delegate.o)
duplicate symbol _OBJC_METACLASS_$_AppDelegate in:
Build/Intermediates/Demo.build/Debug-iphoneos/Demo.build/Objects-normal/arm64/AppDelegate.o
Build/Products/Debug-iphoneos/libPods-Reflexion.a(app_delegate.o)
ld: 2 duplicate symbols for architecture arm64
clang: error: linker command failed with exit code 1 (use -v to see invocation)

という事で AppDelegate クラスが2つあるよと怒られてしまいました。自動生成された AppDelegate.h と .m を、あとついでに ViewController.h と .m もプロジェクトから消してしまいましょう。

そしてもう一度ビルド。

main.m:10:9: fatal error: 'AppDelegate.h' file not found

#import "AppDelegate.h"
^
1 error generated.

おっと、main.m から今消した AppDelegate.h を import しようとして当然ですがエラーになってしまっていますね。main.m を下のように書き換えましょう。


main.m

#import <Reflexion.h>

int main(int argc, char * argv[]) {
[Reflexion start];
return 0;
}


そしてもう一度ビルド。すると成功!やったー!!

と思って実行したら今度は Xcode のアウトプットに下のエラーが表示されアプリが落ちてしまいました。

Exception: #<LoadError: cannot load such file -- main.rb>

そして最後の仕上げです。プロジェクトに以下のような main.rb というファイルを追加しましょう。


main.rb

require 'reflexion/include'

draw do
text 'Hello, world!', 100, 100
end


無事 iPhone に文字が表示されました!イェイ!

Kobito.YQCuI9.png


作ったゲームを iPhone で動かす

ではいよいよ main.rb の内容を先ほど仕上げたジャンプアクションゲームのスクリプトに差し替えてみましょう。

そして実行すると・・・・・・、あっさりとゲームが iPhone で動いてしまいました!

Kobito.grnEJ9.png

・・・が、キー入力が無いためプライヤーキャラクタを操作出来ません!タッチイベントに対応しなければゲームになりませんね。以下のコードを追加しましょう。

pointer do                   # タッチイベントやマウスイベントが発生するたびに呼ばれる処理

next unless down? || up? # 指が触れた時と離れた時のみ処理したい
if y < window.height / 2 # タッチ位置が画面の上半分だったら?
$player.jump if down? # ジャンプする
elsif x < window.width / 2 # タッチ位置が画面下半分のさらに左半分だったら?
$left = down? # 左方向に移動する
else # タッチ位置が画面下半分のさらに右半分だったら?
$right = down? # 右方向に移動する
end
end

これで無事、タッチで操作できるようになりました!めでたしめでたし!!

Ruby で作ったゲームを iPhone で動かしてみた


さいごに

この仕組で作ったアプリがアップルの審査を通過出来るかはまだ試していないのでわかりませんが、最近の状況を見るとおそらく内容に問題が無ければ仕組みとしては審査は通過出来るのではないかと思っています。なのでもうちょっとちゃんとゲームとして体裁をまとめてまずは一個、ゲームアプリをストアに公開してみたいですね。

以上です。では来年の Advent Calendar 2016 でまたお会いしましょう!(笑