Processing ベースの2Dゲームエンジン for CRuby の紹介
こちらは Ruby Advent Calendar 2023、10日目の記事です。
はじめに
RubySketch という CRuby 向けの2Dゲームエンジンを作ってます。
今日はこの RubySketch を使って簡単にゲームを作る方法を紹介したいと思います。
作例
機能紹介の前に、このゲームエンジンでどれくらいのゲームが作れるのか、作品例を紹介します。
ソリティアカードゲーム
ルビーソリティア(RubySolitaire)という名前で今年の夏に AppStore に公開しました。
何の変哲もないただのクロンダイク・ソリティアですが、このゲームエンジンで作った作品を Apple の AppStore に公開するというデモンストレーション[1]のために作りました。
この動画は Mac 上で実行している様子ですが、スクリプトをそのまま iOS アプリとしてパッケージングしストア公開しています。
デモ用アプリとは書きましたが、ソリティアとしてちゃんと作ってありますので、よかったら是非ダウンロードして遊んでみてください。
ルビーソリティア - クロンダイク トランプカードゲーム
スイカゲーム風サンプルゲーム
最近流行った[2]スイカゲーム風サンプルスクリプトです。ソースコードはそのうちゲームエンジンに同梱する予定[3]でいます。
見た目は簡素ですがそれっぽい動きをちゃんとしてるので結構遊べます。
エンジン同梱のサンプルゲーム
ほか、ゲームエンジンにサンプルコードとして同梱してる簡単なゲーム達です。
Flappy Bird 風な鳥のゲーム
マインスイーパー
絵文字でシューティングゲーム
機能紹介
ではさっそく、このゲームエンジンでできることを簡単に紹介していきたいと思います。
ウィンドウの表示
まずはウィンドウを表示してみます。
# ゲームエンジンを読み込む
require 'rubysketch'
# おまじない
using RubySketch
# draw ブロックは毎秒60回呼ばれる
draw do
# まだ何もしない
end
ウィンドウのタイトルを変更して、ウィンドウの背景色も変更します。
require 'rubysketch'
using RubySketch
# setup ブロックは起動時に一度だけ呼ばれる
setup do
# ウィンドウタイトルを指定
setTitle('Hello, World!')
# ウィンドウサイズを変更
size(320, 240)
end
draw do
# 背景色を RGB で指定
background(200, 100, 50)
end
画像の読み込みと描画
ゲームで最も重要な画像の描画です。画像ファイルを読み込んで描画してみます。
サンプルで読み込む画像 char01.png
require 'processing'
using Processing
# 画像を読み込む
char = loadImage('char01.png')
draw do
background(100, 100, 100)
# 画像を原点(左上)に5倍に拡大して描画
image(char, 0, 0, char.width * 5, char.height * 5)
end
画像は任意の位置に描画できるので、マウスの座標に描画してみます。その際、描画の基準位置を画像の中心に指定します。
require 'rubysketch'
using RubySketch
char = loadImage('char02.png')
draw do
background(100, 100, 100)
# 画像描画の基準位置を画像中心に設定
imageMode(CENTER)
# マウス位置(mouseX、mouseY)に画像の大きさを5倍にして描画
image(char, mouseX, mouseY, char.width * 5, char.height * 5)
end
図形の描画
Processing をベースにしているので図形も簡単に描画できます。
矩形の描画
四角形を描画します。
require 'rubysketch'
using RubySketch
draw do
background(100, 100, 100)
# 塗りつぶしの色を指定
fill(255, 100, 100)
# 枠線の色を指定
stroke(100, 255, 100)
# 枠線の太さを指定
strokeWeight(30)
# 線の折れる部分の形状を指定
strokeJoin(ROUND)
# 矩形を位置と大きさを指定して描画
rect(100, 100, 300, 200)
end
塗りつぶしと枠線は個別に無効化できます。まずは枠線無し。
require 'rubysketch'
using RubySketch
draw do
background(100, 100, 100)
fill(255, 100, 100)
# 枠線を描画しない
noStroke()
rect(100, 100, 300, 200)
end
塗りつぶさない。
require 'rubysketch'
using RubySketch
draw do
background(100, 100, 100)
stroke(100, 255, 100)
strokeWeight(30)
strokeJoin(ROUND)
# 塗りつぶさない
noFill()
rect(100, 100, 300, 200)
end
円の描画
丸い形状も描画できます。
require 'rubysketch'
using RubySketch
draw do
background(100, 100, 100)
fill(255, 100, 100)
stroke(100, 255, 100)
strokeWeight(30)
# 円形状を位置と大きさを指定して描画
ellipse(100, 100, 300, 200)
end
ポリゴンの描画
任意の多角形ポリゴンも描画できます。
require 'rubysketch'
using RubySketch
draw do
background(100, 100, 100)
fill(255, 100, 100)
stroke(100, 255, 100)
strokeWeight(30)
strokeJoin(ROUND)
# ポリゴンの描画を開始
beginShape()
# 任意の数の頂点を指定
vertex(100, 100)
vertex(400, 100)
vertex(300, 400)
vertex(200, 200)
vertex(100, 300)
# ポリゴンの描画を完了
endShape()
end
endShape()
に CLOSE
を渡せば始点と終点がつながった線も描画できます。
require 'rubysketch'
using RubySketch
draw do
background(100, 100, 100)
fill(255, 100, 100)
stroke(100, 255, 100)
strokeWeight(30)
strokeJoin(ROUND)
beginShape()
vertex(100, 100)
vertex(400, 100)
vertex(300, 400)
vertex(200, 200)
vertex(100, 300)
# CLOSE を渡すことで始点と終点がつながる
endShape(CLOSE)
end
テキストの描画
もちろんテキストも描画できます。
require 'rubysketch'
using RubySketch
draw do
background(50, 50, 50)
# テキストの色は fill() で指定する
fill(255, 255, 200)
# フォントを指定
textFont('noteworthy')
# フォントサイズを指定
textSize(50)
# 位置を指定してテキストを描画
text('Hello, World!', 10, 100)
# もちろん日本語も問題なく表示
text('はろーわーるど!', 10, 200)
end
textAlign()
で配置も指定できます
require 'rubysketch'
using RubySketch
draw do
background(50, 50, 50)
fill(255, 255, 200)
textFont('noteworthy')
textSize(20)
# 水平方向は右寄せ、垂直方向で下寄せでテキストを描画
textAlign(RIGHT, BOTTOM)
text('右下に描画します!', 100, 100, 300, 200)
# 縦横ともに矩形内の中央にテキストを描画
textAlign(CENTER, CENTER)
text('中央に描画します!', 100, 100, 300, 200)
# 位置(100, 100)、幅 300、高さ 200 の枠を描画
noFill()
stroke(255, 255, 255)
rect(100, 100, 300, 200)
end
スプライトの表示
2Dゲームエンジンとしてスプライト機能もあるので、スプライトを表示してみます。
スプライト画像 char03.png
require 'rubysketch'
using RubySketch
# スプライト画像を読み込む
char = loadImage('char03.png')
# スプライトを作成
sp = createSprite(0, 0, image: char)
draw do
background(100, 100, 100)
# スプライトを描画
sprite(sp)
end
# マウスがドラッグされるたびに呼ばれる
mouseDragged do
# スプライトの中心位置をマウス座標で更新する
sp.center = [mouseX, mouseY]
end
スプライトに速度を与えると自動で移動します。
require 'rubysketch'
using RubySketch
char = loadImage('char03.png')
sp = createSprite(0, 0, image: char)
draw do
background(100, 100, 100)
sprite(sp)
end
# マウスのボタンが押されるたびに呼ばれる
mousePressed do
# マウス座標のベクトル
mouse = createVector(mouseX, mouseY)
# スプライトの中心位置
center = sp.center
# 速度を計算
velocity = (mouse - center) / 10
# 速度を設定
sp.velocity = velocity
end
一定間隔でスプライト画像を差し替えることでアニメーションさせることができます。
スプライトシート画像 char04.png
require 'rubysketch'
using RubySketch
# スプライトシート画像を読み込む
chars = loadImage('char04.png')
# スプライトの大きさを画像より小さい 100x100 にして、画像の一部分をスプライト画像とする
# 画像の表示範囲は offset を指定して変更可能
sp = createSprite(0, 0, 100, 100, image: chars, offset: [0, 0])
# スプライトシートの各画像の Y 座標を配列にしておく
yoffsets = [303, 606, 707, 0, 202, 101, 404, 505]
draw do
background(100, 100, 100)
# 4倍の大きさでスプライトを描画する
scale(4, 4)
sprite(sp)
end
# 0.1秒経過するたびに呼ばれる
setInterval(0.1) do
# Y 座標の配列をローテーションさせる
yoffsets.rotate!
# スプライト画像の表示範囲を offset で更新する
sp.offset = [sp.offset.x, yoffsets[0]]
end
キーボードのカーソルキーでスプライトを操作できるようにしてみます。
require 'rubysketch'
using RubySketch
chars = loadImage('char04.png')
sp = createSprite(0, 50, 100, 100, image: chars, offset: [0, 0])
yoffsets = [303, 606, 707, 0, 202, 101, 404, 505]
# 押しているキーを保持する変数
key = nil
draw do
background(100, 100, 100)
# 押しているキーを表示する
textSize(40)
text("keyPressed = '#{key&.to_s&.upcase || '-'}'", 20, 100)
# 3倍の大きさでスプライトを描画する
scale(3, 3)
sprite(sp)
end
# スプライトをアニメーションさせる
setInterval(0.1) do
yoffsets.rotate!
sp.offset = [sp.offset.x, yoffsets[0]]
end
# キーボードのキーが押されるたびに呼ばれる
keyPressed do
# 現在押しているキーを保持する変数を更新する
key = keyCode
# 押されたキーに応じて処理をする
case key
when LEFT
# 左カーソルキーが押されたらスプライトの速度を左方向に更新する
sp.velocity = [-100, 0]
when RIGHT
# 右カーソルキーが押されたらスプライトの速度を右方向に更新する
sp.velocity = [100, 0]
end
end
# キーが離されるたびに呼ばれる
keyReleased do
# スプライトの速度をゼロにする
sp.velocity = [0, 0]
# 現在押しているキーを保持する変数をクリアする
key = nil
end
物理演算
2Dゲーム用物理エンジンとして広く利用されている Box2D を組み込んでありますので、重力を設定したりスプライト同士の衝突処理なども簡単に実装できます。
require 'rubysketch'
using RubySketch
# ウィンドウの大きさを取得
w, h = width, height
# マウスクリックで生成するスプライトを入れておく配列
objs = []
# 地面スプライト
ground = createSprite(0, h - 50, w, 50)
# 地面の摩擦係数を設定
ground.friction = 1
# 重力を Y 軸下方に設定
gravity(0, 1000)
draw do
background(100)
# マウスクリックで生成したスプライトをまとめて描画
sprite(objs)
# 地面の色を設定
fill(100, 200, 200)
# 地面スプライトを描画
sprite(ground)
end
# マウスボタンが押されるたびに呼ばれる
mousePressed do
# スプライトを 50x50 の大きさで生成
sp = createSprite(0, 0, 50, 50)
# スプライトの中心位置をマウス座標で更新
sp.center = [mouseX, mouseY]
# スプライトを物理エンジンで動かす対象に設定する
sp.dynamic = true
# スプライトの摩擦係数を設定する
sp.friction = 1
# 配列に追加
objs << sp
end
物理演算によって、キャラクターのジャンプなども簡単に実装できます。
require 'rubysketch'
using RubySketch
# 重力を設定する
gravity(0, 1000)
# プレイヤーキャラクターのスプライト
player = createSprite(0, 0, 50, 50)
player.dynamic = true
grounds = [
# 地面スプライト
createSprite(0, height - 50, width, 50),
# 高い位置の床スプライト
createSprite(width / 2, height - 200, width / 2, 10)
]
draw do
background(100, 100, 100)
# プレイヤーキャラクターのスプライトを描画
sprite(player)
# 地面と床スプライトをまとめて描画
fill(100, 200, 200)
sprite(grounds)
end
keyPressed do
case keyCode
# 左右カーソルキーが押されたらプレイヤースプライトの水平方向の速度を更新する
when LEFT then player.vx = -200
when RIGHT then player.vx = 200
# スペースキーが押されたら、プレイヤースプライトの速度の Y 成分を上方向に更新する
when SPACE then player.vy = -600
end
end
keyReleased do
case keyCode
# 左右カーソルキーが離されたらプレイヤースプライトの水平方向の速度をクリアする
when LEFT, RIGHT then player.vx = 0
end
end
衝突判定
スプライト同士の衝突処理も簡単に書けます。
地面を左右に分割し、右側の地面と当たったときだけスプライトの位置の Y 座標を 0 に更新します。
require 'rubysketch'
using RubySketch
w, h = width, height
objs = []
gravity(0, 1000)
# 左半分の地面スプライト
left = createSprite(0, h - 50, w / 2, 50)
# 右半分の地面スプライト
right = createSprite(w / 2, h - 50, w / 2, 50)
draw do
background(100)
sprite(objs)
# 左半分の地面スプライトを描画する
fill(100, 200, 200)
sprite(left)
# 右半分の地面スプライトを描画する
fill(200, 100, 200)
sprite(right)
end
mousePressed do
sp = createSprite(0, 0, 50, 50)
sp.center = [mouseX, mouseY]
sp.dynamic = true
objs << sp
# 他のスプライトと衝突するたびに呼ばれる
sp.contact do |o|
# 衝突相手が右側の地面だったら、スプライトの Y 座標を 0 にする
sp.y = 0 if o == right
end
end
交差した相手スプライトと、衝突するかどうかを決定する処理も簡単に実装することができます。
上のキャラクターのジャンプを実装したサンプルコードを改変して、高い位置にある床に、下からジャンプしたときは衝突せずすり抜けるようにしてみます。(プラットフォーマー系ゲームでよくあるやつです)
require 'rubysketch'
using RubySketch
gravity(0, 1000)
# 地面スプライト
ground = createSprite(0, height - 50, width, 50),
# 高い位置の床スプライト
floor = createSprite(width / 2, height - 200, width / 2, 10)
# プレイヤースプライト
player = createSprite(0, 0, 50, 50)
player.dynamic = true
# 衝突直前に呼ばれ、その相手と衝突するかどうかを決定する
# true を返せば衝突し、false を返せば衝突しない
player.contact? do |o|
if o == floor
# スプライトの交差相手が「高い位置の床」で、ジャンプしていなければ衝突する
# プレイヤーの速度のY軸方向の値が0、またはプラスの値(下方向)ならジャンプしていない
player.vy >= 0
else
# スプライトの交差相手が「高い位置の床」以外なら全て衝突する
true
end
end
draw do
background(100, 100, 100)
sprite(player)
fill(100, 200, 200)
sprite(ground, floor)
end
keyPressed do
case keyCode
when LEFT then player.vx = -200
when RIGHT then player.vx = 200
when SPACE then player.vy = -600
end
end
keyReleased do
case keyCode
when LEFT, RIGHT then player.vx = 0
end
end
サウンドの再生
ゲームエンジンですから、サウンドファイルを読み込んで再生することもできます。
require 'rubysketch'
using RubySketch
w, h = width, height
# バスドラムのスプライトを作成
kick = createSprite(0, 0, w, h / 3)
# バスドラムのスプライトの描画処理をカスタマイズする
kick.draw do
fill(255, 200, 200)
rect(0, 0, kick.width, kick.height)
end
# バスドラムのスプライトがマウスでクリックされたら呼ばれる
kick.mousePressed do
# バスドラムの音を読み込んで再生する
loadSound('Kick.mp3').play()
end
# スネアドラムのスプライトを作成
snare = createSprite(0, h / 3, w, h / 3)
# スネアドラムのスプライトの描画処理をカスタマイズする
snare.draw do
fill(200, 255, 200)
rect(0, 0, snare.width, snare.height)
end
# スネアドラムのスプライトがマウスでクリックされたら呼ばれる
snare.mousePressed do
# スネアドラムの音を読み込んで再生する
loadSound('Snare1.mp3').play()
end
# オープンハイハットのスプライトを作成
hihat = createSprite(0, h / 3 * 2, w, h / 3)
# オープンハイハットのスプライトの描画処理をカスタマイズする
hihat.draw do
fill(200, 200, 255)
rect(0, 0, hihat.width, hihat.height)
end
# オープンハイハットのスプライトがマウスでクリックされたら呼ばれる
hihat.mousePressed do
# オープンハイハットの音を読み込んで再生する
loadSound('OpenHiHat.mp3').play()
end
draw do
background(100)
sprite(kick, snare, hihat)
end
使い方
順番が前後しますが、実際に手元で使ってみる方法を説明します。
macOS で動かす方法
まず、RubySketch は RubyGems で公開しているので gem
コマンドでインストールすることができます。
$ gem install rubysketch
gem install
が成功したら、お好きなテキストエディターで上に書いたようなスクリプトを書いて .rb
ファイルとして保存してください。たとえば hoge.rb
というファイル名でスクリプトを保存したら実行方法は次のようになります。
$ ruby hoge.rb
普通に Ruby のスクリプトとして実行するだけですね。これで RubySketch ゲームエンジンで作ったゲームが実行できます。
$ gem install rubysketch # rubysketch gem をインストール
...
$ cat hoge.rb # スクリプトの中身を確認
require 'rubysketch'
using RubySketch
setup {
size(600, 500)
setTitle('Rotating Rectangle')
}
draw {
background(100)
fill(255, 100, 100)
stroke(100, 255, 100)
strokeWeight(20)
translate(width / 2, height / 2)
rotate(frameCount / 10.0)
rectMode(CENTER)
rect(0, 0, 200, 100)
}
$ ruby hoge.rb # スクリプトを実行
iPhone/iPad で動かす方法
このゲームエンジンは、単体アプリとしても AppStore に公開していますのでそのアプリ上でスクリプトを実行することもできます。
やり方としてはまず、iPhone/iPad で同じ名前の RubySketch というアプリ[4]をインストールします。
RubySketch - Processing互換の開発環境
アプリの Store ページの QRコードです。こちらかもどうぞ
アプリをインストールできましたら起動し、以下の手順でアプリを操作してください。[5]
最後の三角の実行ボタンをタップすると、このように iPhone/iPad 上で同じようにスクリプトを実行することができます。
「下からはすり抜ける床のスクリプト」も iPhone で実行してみるとこんな感じになります。
keyPressed() などのキーボード関係のブロックを定義しているので、自動でバーチャルパッドが表示されています。
簡単なプラットフォーマーゲームの作成
ということで、ここまでに紹介した機能を使えば簡単なプラットフォーマーゲームが作れそうなので、ミニマムなものを作ってみました。
require 'rubysketch'
using RubySketch
# プレイヤーキャラクター用スプライトシート画像を読み込む
charsImage = loadImage('char04.png')
# ゴールオブジェクトの画像を読み込む
goalImage = loadImage('goal.png')
# 着地するときの音を読み込む
landingSound = loadSound('landing.m4a')
# ジャンプするときの音を読み込む
jumpSound = loadSound('jump.m4a')
# ゲームクリアするときの音を読み込む
clearSound = loadSound('clear.m4a')
# プレイヤーのスプライト画像のアニメーション用のオフセット値
yoffsets = [303, 606, 707, 0, 202, 101, 404, 505]
# プレイヤーを反転するかどうかのフラグ
flipPlayer = false
# 重力を設定
gravity(0, 1000)
# 地面スプライトを作成
ground = createSprite(0, height - 50, width, 50),
# 高い位置の床スプライトを作成
floors = [
createSprite(width / 2, height - 200, width / 2, 10),
createSprite(width / 3, height - 350, width / 3, 10),
]
# ゴールオブジェクトのスプライトを作成
goal = createSprite(50, 30, 30, 30, image: goalImage, offset: [10, 10])
# プレイヤースプライトを作成
player = createSprite(10, 400, 50, 44, image: charsImage)
# 物理演算で移動するオブジェクトにする
player.dynamic = true
# スプライトが回転しないようにする
player.fixAngle()
# プレイヤーのアニメーション
setInterval(0.1) do
yoffsets.rotate!
player.offset = [30, yoffsets[0] + 40]
end
# プレイヤースプライトの描画処理をカスタマイズする
player.draw do |&draw|
if flipPlayer
# スプライトの描画を反転する
translate(player.w, 0)
scale(-1, 1)
end
# スプライトのデフォルトの描画処理を実行する
draw.call
end
player.contact do |o|
case o
when *ground, *floors
# 地面、床スプライトと衝突したら着地サウンドを再生する
landingSound.play()
when goal
# ゴールオブジェクトと衝突したらゴールスプライトを削除
removeSprite(goal)
goal = nil
# クリアサウンドを再生する
clearSound.play()
end
end
player.contact? do |o|
# 高い位置の床スプライトは下から通過できる
if floors.include?(o)
player.vy >= 0
else
true
end
end
draw do
background(100, 100, 100)
# 地面と床を描画
fill(100, 200, 200)
sprite(ground, *floors)
# プレイヤーを描画
fill(255, 255, 255)
sprite(player)
if goal
# ゴールオブジェクトがまだ有れば描画
sprite(goal)
else
# ゴールオブジェクトが無ければクリア表示
textSize(100)
textAlign(CENTER, CENTER)
text('CLEAR!', 0, 0, width, height)
end
end
keyPressed do
case keyCode
when LEFT
# 左キーが押されたらプレイヤーの速度を左に更新
player.vx = -200
# プレイヤーを反転するフラグを立てる
flipPlayer = true
when RIGHT
# 右キーが押されたらプレイヤーの速度を右に更新
player.vx = 200
# プレイヤーを反転するフラグを false に
flipPlayer = false
when SPACE
# スペースキーが押されたらジャンプ
player.vy = -600
# ジャンプサウンドを再生する
jumpSound.play()
end
end
keyReleased do
case keyCode
when LEFT, RIGHT
# 左右キーが離されたら水平方向の速度を0にする
player.vx = 0
end
end
どうでしょうか、サウンドも付けたのでちゃんとゲームらしくなってますよね。
まとめ
ということで、私が個人的に開発している 2Dゲームエンジン RubySketch の紹介でした。
Processing ベースということで、グラフィックス周りの API は Processing や p5.js と同じ関数が使えます[6]ので、Processing を触ったことがある方でしたら割りとすんなりと使い始められるかなと思います。[7]
またゲーム開発だけでなく、Creative Coding[8] 環境としても使っていただけます[9]ので、ゲーム以外にもインタラクティブアート作品やデータビジュアライゼーションなどにも活用していただけたら嬉しいです。
参考リンク
- Processing Gem ソースコード
- RubySketch Gem ソースコード
- RubySolitaire ソースコード
- Processing 公式サイト
- p5.js 公式サイト
- #つぶやきProcessing
- OpenDuelyst (ライセンスが CC0 なのでこちらの素材を使わせていただきました)
過去の関連記事
- 自前 Processing 実装の互換性検証に p5.rb を使ってみた話 (2023)
- 3ヶ月間のフルタイム個人開発を終えて。(そして社会復帰へ・・・ (2023)
- RubySketch ゲームエンジン化計画 〜 物理演算付きスプライトクラスの追加など (2022)
- Ruby で Processing する gem(とアプリ)の紹介 (2020)
- iPhone の「カメラ入力」でおもしろビデオカメラを作ろう (2020)
- CRuby でゲームを作って iPhone で動かそう (2015)
- Ruby 用自作 GUI ツールキットに Box2D を組み込んだらブロック崩しゲームが簡単に作れた話 (2014)
- CRuby インタプリタの CocoaPod を作ったので紹介します (2014)
-
RubySolitaire はソースコードも公開しています →→ github.com/xord/solitaire ↩︎
-
Ruby Advent Calendar 6日目の DXOpalでスイカゲームみたいなのを作った でもスイカゲーム作られてますね。 ↩︎
-
この RubySketch アプリは2022年の第14回フクオカRuby大賞 でありがたいことに優秀賞をいただいております ↩︎
-
最後の hoge.rb 編集画面では実際にスクリプトを入力する必要がありますが、Mac をお使いであれば Mac 側でスクリプトをコピーし、アプリ上でペーストすると簡単です ↩︎
-
Processing や p5.js と同じ関数が使えますので、グラフィック周りの関数の使い方を調べたいときは Processing のリファレンスや p5.js のリファレンスをを見ていただくのがはやいかもしれません ↩︎
-
RubySketch iOS アプリにはたくさんのサンプルスクリプトや参考になるソースコードを同梱していますので、そちらを参考にしていただくのもおすすめです ↩︎
-
X(Twitter) 上では #つぶやきProcessing などでもたくさんの作品を見ることができます ↩︎
-
実は RubySketch アプリはリリース当初、ゲームエンジンではなく Creative Coding 用アプリとして作っていました。名前に Sketch が付いてるのもその名残です ↩︎
Discussion