1. Qiita
  2. 投稿
  3. DXRuby

【DXRuby】使えそうで使えない、ちょっと使える自作クラス群

  • 1
    いいね
  • 0
    コメント

はじめに

この記事はRuby Game Developing Advent Calendar 2016の24日目です。残すところあといよいよ2日となりました。
ところで世間的にはクリスマスイブですがいかがお過ごしでしょうか?私は今日も明日も予定がありません。

…さてRubyでゲームを作りましょう。

この記事について

今回は私が個人的にDXRubyを使ってゲームを作っている際に、作ったクラスの説明です。
ほぼ自分用に作っていったものなので、そのまま移植してもほぼ使えることはないかと思います。
目的としては、始めたばかりの人は「こういう作り方がある」と参考にしていただいて、
ゲーム制作をしている人からは「ここはこうしたらどうか」とご意見をうかがいたいと思いました。(ほぼこっちだと思うけど)

一応、ここに載っけるのは改変等々自由にしていただいて構いません。不具合は自己責任で。
なお個人製作したゲームからのコピペなので、あるのに使っていない変数や、機能していないメソッド等ありますがどうか温かい目で見てください。

Layerクラス

私が作った中では一番年季が入ったクラスです。かれこれ2年くらい弄りまくってます。

layer.rb

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自体に描画をさせることで管理しやすくしています。具体的には、

example.rb
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の名前をシンボルで設定します。
その後に、

example.rb
Layer.update_rank = [:example_layer1, :example_layer2]
Layer.draw_rank = [:example_layer2, :example_layer1]

のように、設定した名前を配列に格納します。このようにすると左から順番にupdate、drawが呼ばれていきます。見ての通り、更新と描画の順番を変えることもできます。また、

example.rb
Layer.draw_rank =   [:example_layer2,  [:example_layer3, :example_layer4], :example_layer1]

のようにさらに配列を使って指定すると、その間(:example_layer3と:example_layer4)は更新順や描画順が無くなり、純粋にSpriteが格納された順番になります。レイヤーの中身が結合されるイメージです。

二つ目の機能は当たり判定のグループ化です。

example.rb
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というメソッドがありますが、このメソッドは"パッドのキーをキーボードのキーに割り当てる"というもので、それぞれを独立させるには少し工夫が必要でした。

InputManeger.rb
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モジュール

もはやクラスではなくなりました。

Sprite_Extention.rb
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しない方がいいと思います。

形にすらならなかったもの

何かしらの形にはできなかったのですがアイデアとして置いておきます。

Noname.rb
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枚の画像が合成されて一つの画像になります。
さらに、

Noname2.rb
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でアニメーションする画像同士を合成することができます。
これを使えば例えば、キャラの画像と毒のダメージの効果画像を作っておけば合成するだけで済みますし、敵キャラなどにも使い回すことができます。
sample.gif
左から3つが合成元で、一番右が合成結果です。

最後に

群っていうほど群れてなかったですね。説明するのがとても難しかった…。
今回初めて参加して、記事も初めて書くのですが毎年どんどんレベルが上がっていってこんな記事で大丈夫か不安ですが、ここまでお付き合いしてくださった方々に感謝したいと思います。
明日はいよいよ25日です。aoitakuさんの「よくわかる Ruby ゲーム開発のこれから」です。Ruby3の話題なんかも出てくるのでしょうか。楽しみです。

おまけ

最近、流行りの擬人化ブームに乗っかって、
dxruby.jpg
はい、DXRubyを擬人化してみました。
こんなことして誰が喜ぶかわかりませんが、とにかくやりたかっただけです。以上です。