#はじめに
この記事はRuby Game Developing Advent Calendar 2016の24日目です。残すところあといよいよ2日となりました。
ところで世間的にはクリスマスイブですがいかがお過ごしでしょうか?私は今日も明日も予定がありません。
…さてRubyでゲームを作りましょう。
#この記事について
今回は私が個人的にDXRubyを使ってゲームを作っている際に、作ったクラスの説明です。
ほぼ自分用に作っていったものなので、そのまま移植してもほぼ使えることはないかと思います。
目的としては、始めたばかりの人は「こういう作り方がある」と参考にしていただいて、
ゲーム制作をしている人からは「ここはこうしたらどうか」とご意見をうかがいたいと思いました。(ほぼこっちだと思うけど)
一応、ここに載っけるのは改変等々自由にしていただいて構いません。不具合は自己責任で。
なお個人製作したゲームからのコピペなので、あるのに使っていない変数や、機能していないメソッド等ありますがどうか温かい目で見てください。
#Layerクラス
私が作った中では一番年季が入ったクラスです。かれこれ2年くらい弄りまくってます。
class Layer
#すべてのレイヤーのハッシュ
@@layers = {}
@@collision_group = []
#アップデート順を定めた配列 シンボルで格納
@@update_rank = []
#ドロー順を定めた配列 シンボルで格納
@@draw_rank = []
#アップデート順を適用した配列レイヤーそのもの
#@@update_ranked_layer = []
#ドロー順を適用した配列レイヤーそのもの
#@@draw_ranked_layer = []
#レイヤー全体のオブジェクトのすべて
@@all_layers_object = []
#アップデート順を適用したオブジェクトの集まり
@@update_object = []
#ドロー順を適用したオブジェクトの集まり
@@draw_object = []
attr_accessor :layer, :name, :update_index, :draw_index, :layerobject
#途中から変更することを想定していない
def initialize(w, h, name, x = 0, y = 0)
@update_index = @@update_object.size
@draw_index = @@draw_object.size
@@update_object << []
@@draw_object << []
#そのレイヤーの名前
@name = name
@layerobject = []
@@update_rank << name
@@draw_rank << name
@@layers[name] = self
end
def <<(other)
@layerobject.concat([other])
@@all_layers_object.concat([other])
@@update_object[@update_index].concat([other])
@@draw_object[@draw_index].concat([other])
end
def self.layer(name)
end
def self.target(name)
end
def self.objects(name)
return @@layers[name].layerobject
end
def update
self.layerobject.each{|e| e.update}
end
def draw
self.layerobject.each{|e| e.draw}
end
def dispose
end
def objects
return self.layerobject
end
def all_vanish
self.layerobject.each{|e| e.vanish}
end
def all_send(*pram)
self.layerobject.each{|e| e.send(*pram)}
end
def self.all_object
return @@update_object.flatten
end
def self.all_layers
return @@layers.values()
end
def self.all_update
Sprite.update(@@update_object)
#p @@update_object.flatten.size
#@@update_ranked_layer.each{|e| Sprite.update(e.layerobject)}
end
def self.all_draw
Sprite.draw(@@draw_object)
#@@draw_ranked_layer.each{|e| Sprite.draw(e.object)}
end
def self.all_vanish
@@update_object.flatten.each{|e| e.vanish}#アップデートとドローの配列内のオブジェクトは同じオブジェクトなのでどれかひとつでいい
# @@layers.values.each{|e| e.object.each{|i| i.vanish}}
end
def self.all_clean
Sprite.clean(@@update_object)
Sprite.clean(@@draw_object)
Sprite.clean(@@all_layers_object)
@@layers.values.each{|e| Sprite.clean(e.layerobject)}#レイヤーごとのオブジェクトを破棄
end
def self.update_rank=(ary)
@@update_rank = ary
#@@update_ranked_layer = []#初期化
@@update_rank.each_with_index{|e, i| @@update_rank.each_with_index{|e, i| self.set_rank(i, e)}}
end
def self.update_rank
return @@update_rank
end
def self.draw_rank=(ary)
@@draw_rank = ary
#@@draw_ranked_layer = []#初期化
@@draw_rank.each_with_index{|e, i| self.set_rank(i, e)}
end
def self.set_rank(index, layers)
ary = [index, *layers]
ary[1..ary.size].each{|l| @@layers[l].draw_index = index; @@layers[l].update_index}
end
def self.draw_rank
return @@draw_rank
end
def self.collision_check
@@collision_group.each{|e| Sprite.check(@@layers[e[0]].layerobject, @@layers[e[1]].layerobject)}
end
def self.collision_group=(ary)
@@collision_group = ary
end
def self.collision_group;
return @@collision_group
end
def change_layer(newlayer, obj)
self.layerobject.delete(obj)
@@layers[newlayer].layerobject << obj
end
def clear
end
def clean
Sprite.clean(@layerobject)
end
end
このクラスの主な機能としては次の2つです。
- Spriteオブジェクトをまとめ描画する。
- 当たり判定処理をグループ化し処理を高速化する。
一つ目の機能で、複数のSpriteオブジェクトを一括管理しまとめて処理します。この機能を使うことで、SpriteをLayerに突っ込んだ後は、
- Layer.all_update
- Layer.all_draw
の二つのクラスメソッドを使うだけで全てのSpriteの更新処理と描画処理をすることができます。またLayerクラスの名の通り、このクラスはレイヤー(描画順)を設定することができます。
Spriteが大量に出てくるゲームでは描画順がとても大事になりますが、Sprite#z
で管理するのはかなりきつくなってきます。
そこでこのクラスではLayerを作りそこに描画したいSpriteを設定します。その後Layer自体に描画をさせることで管理しやすくしています。具体的には、
layer1 = Layer.new(600, 600, :example_layer1)
layer1 << Sprite.new(0, 0, "example1.png")
layer2 = Layer.new(600, 600, :example_layer2)
layer2 << Sprite.new(0, 0, "example2.png")
のようにすればLayerオブジェクトが作られ、Spriteオブジェクトがlayer1やlayer2に設定されます。Layerクラスの第3引数にはLayerの名前をシンボルで設定します。
その後に、
Layer.update_rank = [:example_layer1, :example_layer2]
Layer.draw_rank = [:example_layer2, :example_layer1]
のように、設定した名前を配列に格納します。このようにすると左から順番にupdate、drawが呼ばれていきます。見ての通り、更新と描画の順番を変えることもできます。また、
Layer.draw_rank = [:example_layer2, [:example_layer3, :example_layer4], :example_layer1]
のようにさらに配列を使って指定すると、その間(:example_layer3と:example_layer4)は更新順や描画順が無くなり、純粋にSpriteが格納された順番になります。レイヤーの中身が結合されるイメージです。
二つ目の機能は当たり判定のグループ化です。
Layer.collision_group = [
#shot, hit
[:example_layer1, :example_layer2],
[:example_layer1, :example_layer3]
]
グループ化にも、レイヤーの名前を使って設定します。判定処理時に内部で左側はshot、右側はhitメソッドがそれぞれ呼ばれます。
その後、
- Layer.collision_check
を使うことで設定したグループ同士で当たり判定のチェックをします。当然ですが設定していないレイヤー同士はチェックがされません。こうすることによって無駄な判定処理を省き、処理を高速化します。
もともとは二つ別々に作っていたものでしたが、いつの間にか一つに融合していました。ぶっちゃけレイヤーっていう名前の域を超えている気がしないでもないですが。
このクラスはとにかくたくさんのSpriteが動き、当たり判定処理をしなければならないとき(弾幕シューティングとか)に効果的です。逆に判定処理させるものが少ない、Spriteが少ない時などは手間がかかるので遅くなります。
#InputManegerクラス
ゲームを作っていく中で一つ困ったことが起きましてそれは、
"二つ以上のゲームパッドを使うとキーコンフィグがうまくいかない"という問題でした。
具体的には"二つ以上のゲームパッドを使って、同じキーボードのキー定数を別のゲームパッドのキーに割り当てる"というものです。
DXRubyにはInput#set_config
というメソッドがありますが、このメソッドは"パッドのキーをキーボードのキーに割り当てる"というもので、それぞれを独立させるには少し工夫が必要でした。
class InputManeger
def initialize(pad_number = 0)
@pn = pad_number
@key0 = P_BUTTON0
@key1 = P_BUTTON1
@key2 = P_BUTTON2
@key3 = P_BUTTON3
@key4 = P_BUTTON4
@key5 = P_BUTTON5
@key6 = P_BUTTON6
@key7 = P_BUTTON7
@key8 = P_BUTTON8
@key9 = P_BUTTON9
@key10 = P_BUTTON10
@key11 = P_BUTTON11
@key12 = P_BUTTON12
@key13 = P_BUTTON13
@key14 = P_BUTTON14
@key15 = P_BUTTON15
@pup = P_UP
@pleft = P_LEFT
@pright = P_RIGHT
@pdown = P_DOWN
@plup = P_L_UP
@plleft = P_L_LEFT
@plright = P_L_RIGHT
@pldown = P_L_DOWN
@prup = P_R_UP
@prleft = P_R_LEFT
@prright = P_R_RIGHT
@prdown = P_R_DOWN
@pdup = P_D_UP
@pdleft = P_D_LEFT
@pdright = P_D_RIGHT
@pddown = P_D_DOWN
@keycode = {
:P_BUTTON0 => @key0, :P_BUTTON1 => @key1, :P_BUTTON2 => @key2, :P_BUTTON3 => @key3,
:P_BUTTON4 => @key4, :P_BUTTON5 => @key5, :P_BUTTON6 => @key6, :P_BUTTON7 => @key7,
:P_BUTTON8 => @key8, :P_BUTTON9 => @key9, :P_BUTTON10 => @key10, :P_BUTTON11 => @key11,
:P_BUTTON12 => @key12, :P_BUTTON13 => @key13, :P_BUTTON14 => @key14, :P_BUTTON15 => @key15,
:P_UP => @pup, :P_LEFT => @pleft, :P_RIGHT => @pright, :P_DOWN => @pdown,
:P_L_UP => @plup, :P_L_LEFT => @plleft, :P_L_RIGHT => @plright, :P_L_DOWN => @pldown,
:P_R_UP => @prup, :P_R_LEFT => @prleft, :P_R_RIGHT => @prright, :P_R_DOWN => @prdown,
:P_D_UP => @pdup, :P_D_LEFT => @pdleft, :P_D_RIGHT => @pdright, :P_D_DOWN => @pddown
}
end
def update
end
def set_key(newkeycode, oldkey)
newkey = @keycode.key(newkeycode)#変更したいボタンのシンボル
newvalue = @keycode[oldkey] #変更したいボタンの定数
oldvalue = @keycode[newkey]#変更されるボタンの定数
@keycode[oldkey], @keycode[newkey] = oldvalue, newvalue
end
def pad_number
return @pn
end
def pads
return Input.pads(@pn)
end
def x
return Input.x(@pn)
end
def y
return Input.y(@pn)
end
def down?(keycode)
return Input.padDown?(@keycode[keycode], @pn) ? true : false
end
def push?(keycode)
return Input.padPush?(@keycode[keycode], @pn) ? true : false
end
def release?(keycode)
return Input.padRelease?(@keycode[keycode], @pn) ? true : false
end
alias any_down pads
def any_push
pad = []
pad << P_BUTTON0 if Input.padPush?(P_BUTTON0, @pn)
pad << P_BUTTON1 if Input.padPush?(P_BUTTON1, @pn)
pad << P_BUTTON2 if Input.padPush?(P_BUTTON2, @pn)
pad << P_BUTTON3 if Input.padPush?(P_BUTTON3, @pn)
pad << P_BUTTON4 if Input.padPush?(P_BUTTON4, @pn)
pad << P_BUTTON5 if Input.padPush?(P_BUTTON5, @pn)
pad << P_BUTTON6 if Input.padPush?(P_BUTTON6, @pn)
pad << P_BUTTON7 if Input.padPush?(P_BUTTON7, @pn)
pad << P_BUTTON8 if Input.padPush?(P_BUTTON8, @pn)
pad << P_BUTTON9 if Input.padPush?(P_BUTTON9, @pn)
pad << P_BUTTON10 if Input.padPush?(P_BUTTON10, @pn)
pad << P_BUTTON11 if Input.padPush?(P_BUTTON11, @pn)
pad << P_BUTTON12 if Input.padPush?(P_BUTTON12, @pn)
pad << P_BUTTON13 if Input.padPush?(P_BUTTON13, @pn)
pad << P_BUTTON14 if Input.padPush?(P_BUTTON14, @pn)
pad << P_BUTTON15 if Input.padPush?(P_BUTTON15, @pn)
return pad
end
def any_release
pad = []
pad << P_BUTTON0 if Input.padRelease?(P_BUTTON0, @pn)
pad << P_BUTTON1 if Input.padRelease?(P_BUTTON1, @pn)
pad << P_BUTTON2 if Input.padRelease?(P_BUTTON2, @pn)
pad << P_BUTTON3 if Input.padRelease?(P_BUTTON3, @pn)
pad << P_BUTTON4 if Input.padRelease?(P_BUTTON4, @pn)
pad << P_BUTTON5 if Input.padRelease?(P_BUTTON5, @pn)
pad << P_BUTTON6 if Input.padRelease?(P_BUTTON6, @pn)
pad << P_BUTTON7 if Input.padRelease?(P_BUTTON7, @pn)
pad << P_BUTTON8 if Input.padRelease?(P_BUTTON8, @pn)
pad << P_BUTTON9 if Input.padRelease?(P_BUTTON9, @pn)
pad << P_BUTTON10 if Input.padRelease?(P_BUTTON10, @pn)
pad << P_BUTTON11 if Input.padRelease?(P_BUTTON11, @pn)
pad << P_BUTTON12 if Input.padRelease?(P_BUTTON12, @pn)
pad << P_BUTTON13 if Input.padRelease?(P_BUTTON13, @pn)
pad << P_BUTTON14 if Input.padRelease?(P_BUTTON14, @pn)
pad << P_BUTTON15 if Input.padRelease?(P_BUTTON15, @pn)
return pad
end
def any_push?
return self.any_push != []
end
def any_down?
return self.any_down != []
end
def any_release?
return self.any_release != []
end
end
やってることは単純でただ別々に内部のキーコードを保持するだけです。
InputManeger.new
とすれば後はInputの代わりにメソッドを呼び出すだけです。メソッド名はそのままなので移植も簡単です。
InputManeger#set_key
メソッドでキーコンフィグすることができます。キーは元のキーと設定したいキーを入れ替えます。
後ついでにany系も追加しました。
#Sprite_Extentionモジュール
もはやクラスではなくなりました。
module Sprite_Extention
attr_accessor :collision_draw
@@draw_flag = false
def initialize(x = 0, y = 0, image = nil)
super
self.collision_draw = false
end
def draw
super
if (self.collision_draw || @@draw_flag) && self.collision
case self.collision.size
when 2
Window.draw_pixel(self.x + self.collision[0], self.y + self.collision[1], [255, 0, 0])
when 3
self.image.circle_fill(self.x + self.collision[0], self.y + self.collision[1], self.collision[2], [255, 0, 0])
when 4
self.image.box_fill(self.x + self.collision[0], self.y + self.collision[1], self.x + self.collision[2], self.y + self.collision[3], [255, 0, 0])
when 6
self.image.triangle_fill(self.x + self.collision[0], self.y + self.collision[1], self.x + self.collision[2], self.y + self.collision[3], self.x + self.collision[4], self.y + self.collision[5], [255, 0, 0])
#Window.draw(self.x, self.y, i)
end
end
end
def self.collisions_draw?
return @@draw_flag
end
def self.collisions_draw=(f)
@@draw_flag = f
end
end
Extentionと名前がついてますが、Spriteの当たり判定を表示できるようにするだけです。使い方は、Spriteにprepend(includeではダメ)し、Sprite_Extention#collision_draw
で個々のオブジェクトに表示させるか、Sprite_ExtentionSprite_Extention
で一括で表示させるように設定するかです。スケーリングや回転には対応してません。
ゲームを作っていくと当たり判定が見えるようになった方が画面上のデバックがしやすいかなと思って作りました。単純に描画処理が増えるので、デバックの時以外はオフにするかprependしない方がいいと思います。
#形にすらならなかったもの
何かしらの形にはできなかったのですがアイデアとして置いておきます。
TEST_HLSL = <<EOS
texture tex0;
texture tex1;
sampler Samp0 = sampler_state
{
Texture =<tex0>;
};
sampler Samp1 = sampler_state
{
Texture =<tex1>;
};
float4 PS(float2 input : TEXCOORD0) : COLOR0
{
float4 output;
float4 fill;
output = tex2D( Samp0, input );
fill = tex2D( Samp1, input );
output.rgb *= fill.rgb;
return output;
}
technique Normal
{
pass P0
{
PixelShader = compile ps_2_0 PS();
}
}
EOS
mage1 = Image.new(100, 100, [0, 255, 0])
image2 = Image.new(100, 100, [0, 0, 0])
image3 = Image.new(100, 100, [10, 0, 0])
image4 = Image.new(100, 100, [0, 0, 255])
render1 = RenderTarget.new(1000, 1000)
render2 = RenderTarget.new(1000, 1000)
render3 = RenderTarget.new(1000, 1000)
render4 = RenderTarget.new(1000, 1000)
core = Shader::Core.new(TEST_HLSL,{:tex1 => :texture})
shader = Shader.new(core)
Window.loop do
render1.draw(0, 0, image1)
shader.tex1 = image2
render2.draw_shader(0, 0, render1, shader)
shader.tex1 = image3
render3.draw_shader(0, 0, render2, shader)
shader.tex1 = image4
render4.draw_shader(0, 0, render3, shader)
Window.draw(0, 0, render4)
end
とすると4枚の画像が合成されて一つの画像になります。
さらに、
TEST_HLSL = <<EOS
texture tex0;
texture tex1;
sampler Samp0 = sampler_state
{
Texture =<tex0>;
};
sampler Samp1 = sampler_state
{
Texture =<tex1>;
};
float4 PS(float2 input : TEXCOORD0) : COLOR0
{
float4 output;
float4 fill;
output = tex2D( Samp0, input );
fill = tex2D( Samp1, input );
output.rgb *= fill.rgb;
return output;
}
technique Normal
{
pass P0
{
PixelShader = compile ps_2_0 PS();
}
}
EOS
animesprite1 = AnimeSprite.new(100,100)
animesprite1.animation_image = Image.load_tiles($file + '/data/Images/' + 'test_image.png', 4, 1, false)
animesprite1.add_animation(:normal, 60, [0, 1, 2, 3], :normal)
animesprite1.start_animation(:normal)
animesprite2 = AnimeSprite.new(100,100)
animesprite2.animation_image = Image.load_tiles($file + '/data/Images/' + 'test_effect1.bmp', 5, 1, false)
animesprite2.add_animation(:normal, 60, [0, 1, 2, 3], :normal)
animesprite2.start_animation(:normal)
animesprite3 = AnimeSprite.new(100,100)
animesprite3.animation_image = Image.load_tiles($file + '/data/Images/' + 'test_effect2.bmp', 5, 1, false)
animesprite3.add_animation(:normal, 60, [0, 1, 2, 3], :normal)
animesprite3.start_animation(:normal)
render1 = RenderTarget.new(1000, 1000)
render2 = RenderTarget.new(1000, 1000)
render3 = RenderTarget.new(1000, 1000)
render4 = RenderTarget.new(1000, 1000)
core = Shader::Core.new(TEST_HLSL,{:tex1 => :texture})
shader = Shader.new(core)
Window.loop do
animesprite1.update
animesprite2.update
animesprite3.update
render1.draw(0, 0, animesprite1.image)
shader.tex1 = animesprite2.image
render2.draw_shader(0, 0, render1, shader)
shader.tex1 = animesprite3.image
render3.draw_shader(0, 0, render2, shader)
Window.draw(0, 0, render3)
end
とするとAnimeSpriteでアニメーションする画像同士を合成することができます。
これを使えば例えば、キャラの画像と毒のダメージの効果画像を作っておけば合成するだけで済みますし、敵キャラなどにも使い回すことができます。
左から3つが合成元で、一番右が合成結果です。
#最後に
群っていうほど群れてなかったですね。説明するのがとても難しかった…。
今回初めて参加して、記事も初めて書くのですが毎年どんどんレベルが上がっていってこんな記事で大丈夫か不安ですが、ここまでお付き合いしてくださった方々に感謝したいと思います。
明日はいよいよ25日です。aoitakuさんの「よくわかる Ruby ゲーム開発のこれから」です。Ruby3の話題なんかも出てくるのでしょうか。楽しみです。
#おまけ
最近、流行りの擬人化ブームに乗っかって、
はい、DXRubyを擬人化してみました。
こんなことして誰が喜ぶかわかりませんが、とにかくやりたかっただけです。以上です。