Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
DXRubyのSpriteを継承して拡張する方法について

DXRuby Advent Calendar 2013 7日目 title: DXRubyのSpriteを継承して拡張する方法について author: しのかろ(Twitter@shinokaro)

 DXRuby Advent Calendar 2013も7日目。
 1週間目の終わりは“しのかろ”が記事をお送りします。
 世にゲーム・プログラマー多数あり、と言えども実際のコードにおいては、
文章の形で公開されることは稀です。
 ですから、今年のこのイベントで公開される記事を楽しみに毎日を過ごしています。

 前日はt_tutiyaさんが記事を書かれました
 内容は、ノベルゲームエンジンRAGの紹介でした。
 この記事において個人的な注目点は、トランジション処理についてです。

 PCゲームには映像が付き物です。
 ですから、映像分野から表現手法を取り入れることが出来ます。
 従来はPCハードウェアー性能の制限により、映画、TV、ビデオといった映像作品から
表現手法をゲームに取り入れることは難しかったようです。  その理由は、ゲームでは“リアルタイムで処理する”という前提があるためです。
 そのため、映像作品はPCで製作されることが当たり前となりましたがPCゲームでは
映像処理は実装されませんでした。

 最近はゲームのために強力なハードウェアーが搭載されるようになって来ました。
 DXRubyではRubyプログラムから、そのハードウェアーを利用することがて出来ます。
 具体的には、HLSLの使用です。
 HLSLならば、記述したコードはハードウェアーの早さで動きます。
 これを使いこなせればリアルタイムで映像処理を利用できます。
 映像処理の強力さはゲームの演出表現につながります。

 私の記事は、どのようなPCゲームであれ映像出力を使用するならば必ず必要となる
スプライトについてです。  スプライトとはゲーム中で使用する画像に、表示位置や表示の仕方についての情報を
与えたものです。

 DXRubyではSpriteクラスが実装されています。
 これを利用することで簡単に画像を表示できます。
 しかし、自分のゲームエンジンでSpriteクラスを使用するにはいくつか不足している
機能がありました。
 そこで、Rubyのオブジェクト指向言語の特性を最大限に生かし、Spriteクラスを拡張
することにします。

 Rubyでは既存のクラスに直接、自己拡張を施す方法が提供されています。
 それは一番早い解決方法ですが、もしかしたらSpriteクラスが変更されていないこと
を前提にする拡張ライブラリーと共存する必要があるかもしれません。
 ここでは、教科書どおりにSpriteクラスを継承しMySpriteクラスを作成します。

DXRubyのSpriteを継承して拡張する方法について

 自作クラスMySpriteを作成する。このときDXRubyのSpriteを親とする。
 ゲームプログラムでは同一目的のコードであっても、専用クラスを作成する事がある。
 これは、同じ動作、メソッド名を備えているが、内部実装に違いのあるクラスである。
 内部実装を変える理由は、処理を簡略化してパフォーマンスを引き出す場合である。
 これを使用目的に応じて使い分ける。
 このようなクラス群のインスタンスは、ゲーム・スクリプト側からは同じに見える。
 よってスクリプターは、最小限の学習で最大のパフォーマンスを利用できる。

    class MySprite < DXRuby::Sprite

スプライト全体の拡大縮小率を指定するscaleのアクセッサーを定義する。

 Rubyではオブジェクトへのアクセスは、すべてメソッドを使用する。
 よって、インスタンス変数へのアクセスにメソッドを定義する必要がある。
 アクセッサの記述を行うとき、クラス定義内ではattr_accessorメソッドを利用して
代入と出力のアクセッサを定義できる。
 このとき、インスタンス変数"@scale"へのアクセッサscale=, scaleが定義される。
 ただし、インスタンス変数の割り当ては、@scaleへの代入が行われた時である。
 Rubyでは、未定義インスタンス変数のデフォルト値はnilとされているため、
このような事が可能になる。

      attr_accessor :scale

 初期化メソッドをにおいて、キーワード引数を継承される事を考慮して記述する方法。

 継承先クラスで追加されたキーワード引数を無視するため"**kw"を使用する。
(将来のRubyでは、無視するだけなら"**kw"ではなく"**"と記述できるようになる)
 こうしておけば、子クラスから親クラスが受け取れるキーワード引数を考慮せずに
親メソッドを呼び出せる。
 もし、このような配慮を行わなければ子クラスを記述するときに、親クラスの動作に
ついて詳しく知る必要がある。
(でなければ、親クラス側で受け取れないキーワード引数に対してエラーが出る)

 DXRubyのSpriteインスタンス生成時の引数にはスプライトの画面上の座標x,yと
スプライト画像imageを引数として与える。
 Spriteインスタンスには他に多くの描画パラメーターがあるので、
インスタンス生成時にまとめて指定できるようにする。
 初期値を表データとして定義し、それを読み込んでオブジェクトを作るコードを
書く場合には記述が簡単になる。

 image引数をキーワード引数に割り当てなかったのは

  • 必ず必要になる要素
  • 実際のimageオブジェクト割り当てはコードを経由して定義される。
    (イメージの使いまわし)
  • 継承先でイメージを複数取るクラスを作成する予定がある。

以上の理由からイメージ・オブジェクトの受け取りは、通常引数とした。

 ただし、DXRubyのSpriteではimageは必須ではない。
 これに準拠するならばメソッド記述時のimage引数に初期値nilを与えておけばよい。
( 記述例:def initialize(image=nil, ...) )

      def initialize(image, x:nil, y:nil, z:nil, center_x:nil, center_y:nil,
                   angle:nil, scale:1, scale_x:nil, scale_y:nil,
                   alpha:nil, blend:nil, shader:nil, visible:nil, **kw)
        
        # Spriteのメソッド呼び出し。引数はx,yにおいてはnilを入れても0で初期化される。
        # よって、キーワード引数x, yが初期値nilのままでも特別な処理は必要ない。
        
        super(x, y, image)
        
        # DXRubyのSpriteクラスは、アクセッサに与えたオブジェクトをそのまま保存する。
        # 適切なオブジェクトを入れなかった場合、スプライトを表示時にエラーとなる。
        # もし、値を代入しなかった場合は適切な値で初期化される。
        # この振る舞い自体は好ましいので、この仕様を維持する。
        # このため、引数が与えられていないときは、初期化そのものを行わないようにする。
        # また、SpriteクラスはC言語で定義されたクラスである。
        # よってインスタンス変数が存在しないためアクセッサー経由で値を代入する。
        
        self.z=        z        if z
        self.center_x= center_x if center_x
        self.center_y= center_y if center_y
        self.angle=    angle    if angle
        self.scale_x=  scale_x  if scale_x
        self.scale_y=  scale_y  if scale_y
        self.alpha=    alpha    if alpha
        self.blend=    blend    if blend
        self.shader=   shader   if shader
        self.visible=  visible  if visible
        
        # スケール値は問題のない初期値を決定できる。もちろん1倍である。
        # よって引数の初期値は1に設定してある。そして、必ず代入する。
        
        self.scale=    scale
      end

 スプライトのパラメーターをまとめて指定するメソッドの定義。

 キーワード引数を利用することで、“指定しない”パラメーターを表現する。

      def move(**kwarg)
        
        # キーワード引数は、引数名に"**"を付ける事でハッシュとして受け取る。
        # これを利用し、ハッシュの内容にアクセスする。
        
        kwarg.each do |key, val|
        
          # __send__メソッドでselfのアクセッサを呼び出す。
          # (self.__send__のselfは省略可能)
          # 代入アクセッサには"="が末尾に必要になる。
          # よって、キーワード名の末尾に"="を追加する。
          
          __send__(key.to_s + "=", val)
        end
      end

 スプライト自身の幅、高さを取り出すメソッドの定義。

 DXRubyのImageにはこのメソッドが備わっているが、Spriteには存在しない。  Rubyの添付ライブラリーであるForwardableを使用すれば1行で記述できる。

      def width
        self.image.width
      end
      def height
        self.image.height
      end

 現在の表示回転角度をラジアンで出力、代入する単位変換メソッドの定義。

 Spriteは、デグリーで回転角度を持つ。これは一般的な実装だろう。
 一方、スプライトの座標指定などの幾何学計算では数学関数を使用する必要がある。
 そして、数学関数では角度の指定にラジアンの単位を使用する。
 このため、デグリーで値を出力、代入を行うと毎回、単位変換を記述する必要がある。
 この単位変換の記述を避けるため、単位変換メソッドを用意しておく。

 フロート計算が高速化された現在においてもゲームプログラムでは角度表現に
アングルを使用する。
 これはラジアン(フロート型)で角度表現をした場合、フロート型は0から離れる
位置は離散値をとるため、n周回の位置を正確に表現できないからだと思われる。
(これは私の推測)
 幾何学計算の例として、アフィン変換などがある。

      def radian
        Math::PI * self.angle / 180
      end
      def radian=(rad)
        self.angle = rad * 180 / Math::PI
      end

 透明度を現すalphaアクセッサーの値(0~255)を0.0~1.0のフロート値として出力、代入する単位変換メソッドの定義。

 フロートで取り出す必要とするのは、DirectXのピクセルシェーダーでは色や透明度
をフロートで取り扱うため。

      def f_alpha
        self.alpha / 255.0
      end
      def f_alpha=(a)
        self.alpha= a * 255.0
      end

 スプライトの回転中心が表示ウィンドウ内のどこにあるのかを求めるメソッドの定義。

      def real_center_x
        self.x + self.center_x
      end
      def real_center_y
        self.y + self.center_y
      end

 スプライトの回転中心を表示ウィンドウ内のどこにあるのか指定するメソッドの定義。

 デザイナーの視点からはスプライトの中心点を基準に表示位置を決めたほうが便利。

      def real_center_x=(i)
        self.x= i - self.center_x
      end
      def real_center_y=(i)
        self.y= i - self.center_y
      end

 scaleを反映した、実際のscale_x, scale_yを求めるメソッドの定義。

      def real_scale_x
        self.scale_x * self.scale
      end
      def real_scale_y
        self.scale_y * self.scale
      end

 表示メソッドdrawの定義。

 Spriteのdrawは、表示画面への描画を行う。  ここでは独自に定めたパラメーターscaleの影響をSpriteのパラメーター
sclae_x, scale_yに反映させる。

      def draw
        
        # Spriteインスタンスのscale_x, scale_yへのアクセスはアクセッサを利用する。
        # scale_x, scale_yの保護のため一時変数sx, syに値を保存する。
        
        sx = self.scale_x
        sy = self.scale_y
        
        # scale値を反映した、実際のscale_x, scale_yを代入する。
        
        self.scale_x= real_scale_x
        self.scale_y= real_scale_y
        
        # 親クラス、Spriteのdrawを実行する。引数は必要ない。
        
        super
        
        # 一時的に保護したscale_x, scale_yを元に戻す。
        
        self.scale_x= sx
        self.scale_y= sy
        
        # Spriteのdrawメソッドの返り値はselfなので、同じようにselfを返り値にする。
        # Rubyではメソッドの帰り値は最後に評価された値となる。
        
        self
      end

 指定したイメージにスプライトを描画するためのメソッドの定義。

 これは、以下の例で使用する。

  • 複数のスプライトを合成して新しいイメージを作成する。
  • 画面をレイヤー分けしている場合にレイヤー・イメージに描画する。
  • リアルタイム・エフェクトのためにスプライトの変形状態を利用して
    作業用イメージに描画する。
      def target_draw(surface)
        trg = self.target
        self.target = surface
        self.draw
        self.target = trg
        self
      end

 これは、ちょっとしたアイデアで現在のスプライト状態をスプライト生成引数の形で
出力する。思いつきなので実用性などはご容赦願う。

      def to_a
        para_index = [:x, :y, :z, :center_x, :center_y,
                     :angle, :scale, :scale_x, :scale_y,
                     :alpha, :blend, :shader, :visible]
        list = para_index.map! do |name|
          [name, __send__(name)]
        end
        [image, Hash.new(list)]
      end
    end

クラス記述時の注意点

  • 継承先クラスでアクセッサをオーバーライドした時に、その影響を受けるべきか?
  • インスタンス変数に直接アクセスするのか、アクセッサからアクセスするのか?

 クラス記述では、これらを常に意識して記述する。
 正解はなく、記述するクラス、メソッドの特性から考える必要がある。

 Spriteの拡張において、よくないアイデアについて。

 自立したゲームに関するパラメーター変更。
たとえば、ジャンプする動作を自分自身で行う。
この方法は、すばやくゲームを作るとき、かつスプライトオブジェクトの寿命が短い
場合には向いている。  他の情報を元にオブジェクトを動かす場合は、外部情報を取得するために引数か
参照情報を持つことになる。  引数で外部オブジェクトを与えられた場合、そのオブジェクトへのアクセス方法を
考えなければならない。
 参照情報を持つ場合は、スプライトが長期間存在すると、参照先オブジェクトはGC
で回収されなくなる。  また、参照情報の追加、削除の方法を定義する必要があり、その操作をしなければ
ならない。
 何らかのマネージャーへのアクセスを行う場合は、マネージャーへの参照を取得する
方法、操作方法を、そして参照すべき外部情報を選択するための多くの定義が必要にな
る。

 スプライトは、自身の表示に必要な操作に集中し、自身のパラメーター操作は外部の
コードに任せる。

以上、DXRubyのSpriteクラスを継承して自作のSpriteクラスを作成する方法でした。
明日はDXRubyの製作者であるmirichiさんが記事を書きます。

長文、ご拝読ありがとうございました。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.