SVX日記

2004|04|05|06|07|08|09|10|11|12|
2005|01|02|03|04|05|06|07|08|09|10|11|12|
2006|01|02|03|04|05|06|07|08|09|10|11|12|
2007|01|02|03|04|05|06|07|08|09|10|11|12|
2008|01|02|03|04|05|06|07|08|09|10|11|12|
2009|01|02|03|04|05|06|07|08|09|10|11|12|
2010|01|02|03|04|05|06|07|08|09|10|11|12|
2011|01|02|03|04|05|06|07|08|09|10|11|12|
2012|01|02|03|04|05|06|07|08|09|10|11|12|
2013|01|02|03|04|05|06|07|08|09|10|11|12|
2014|01|02|03|04|05|06|07|08|09|10|11|12|
2015|01|02|03|04|05|06|07|08|09|10|11|12|
2016|01|02|03|04|05|06|07|08|09|10|11|12|
2017|01|02|03|04|05|06|07|08|09|10|11|12|
2018|01|02|03|04|05|06|07|08|09|10|11|12|
2019|01|02|03|04|05|06|07|08|09|10|11|12|
2020|01|02|03|04|05|06|07|08|09|10|11|12|
2021|01|02|03|04|05|06|07|08|09|10|11|12|
2022|01|02|03|04|05|06|07|08|09|10|11|12|
2023|01|02|03|04|05|06|07|08|09|10|11|12|
2024|01|02|03|04|05|06|07|08|09|10|11|

2022-08-04(Thu) CoffeeScriptにて回転機能をMixinにより実装す

  意外と飽きずにオリジナルのシューティングの製作をポチポチと進めているのだが、考えれば考えるほど、既存のシューティングに対する気付きが増えていく。

  例えば、R-TYPEは初見でも1面はクリアさせたい作りなんだな、とか、ダライアスはボス以外の作りは適当だな、とかである。反面、ゼビウスは「最初は偵察機が登場する」とか「有人機は自殺的攻撃をしない」とか考えてあることに驚く。そうだよな。それは世界感を感じさせるために有効だよな。

  今回、自分は「ストーリ」を強く感じさせるゲームにしたいので、単に思いつきで敵を作るのではなく、それなりの理由に基づいて登場、行動させたい。そうなると、最初は偵察機だし、その後はミサイル攻撃かな、とか思うわけだ。

  で、ミサイルを作ったのだが、誘導したくなってしまい、斜めに飛ばしたくなってしまった。すると、回転機能が必要になる。先日のテトランはプリレンダだったが、汎用性を考えればリアルタイムレンダを実装するべきだ。しかもキャッシュ付きで、できるだけ軽く。

  とりあえず試験的にCoffeeScriptで実装してみたら、処理はだいぶ軽く、負荷的には問題なさそうだ。後は、どういうパラダイムで実装するか。現状、以下のようなオブジェクト構成になっている。

 class Plane					# 描画面
 class Obj extends Plane			# オブジェクト(スプライト)
 class Ship extends Obj				# 自機
 class Enemy extends Obj			# 敵
 class MissileA extends Enemy			# ミサイル
 class Fixed extends Enemy			# 地形に固定された敵
 class ReconA extends Fixed			# 偵察機

  回転するのは敵に限らず、あらゆるオブジェクトが回転可能にできるべきだ。しかし、そうすると多重継承が必要になってしまう。それを避けるには、一番の根っこであるObjクラスに実装してしまう方法も考えられるが、それは気が進まない。必要に応じて回転機能が追加できるのが望ましい。

  そういう場合、Rubyならミックスイン(mix-in)があるが、CoffeeScriptにはあるのかしらん……と、調べたらあるんだな。というか、正確には「後付けできる」といったところか。

 #---------------------------------------------------------------
 #
 #   クラスの Mixin を可能にする
 #
 #       https://coffeescript-cookbook.github.io/chapters/classes_and_objects/mixins
 #
 mixOf = (base, mixins...) ->
     class Mixed extends base
     for mixin in mixins by -1
         for name, method of mixin::
             Mixed::[name] = method
     Mixed

  上記を冒頭に記述しておいて、

 class Rotnscl					# 回転拡大縮小機能
 class RmissileA extends mixOf Enemy, Rotnscl	# 敵+回転拡大縮小機能

  上記のようなオブジェクト構成にする。それにより、RmissileAは、敵であり、回転機能を持つ、という定義となる。これにて一件落着。

  次は、回転パターンのキャッシュについて考える。処理負荷的にリアルタイムレンダは可能な見通しだが、毎回、回転パターンを作るのは無駄なので、一度作った回転パターンは残しておき、二度目からは勝手にそれを使うようにしたい。つまりはキャッシュだ。

  キャッシュはどう実装されるべきか、といえば、そりゃオブジェクトタイプ、パターン、角度毎に残しておくべきで、個体(インスタンス)毎に行うべきではない。つまり、クラス変数に残しておくべきだ。クラス変数はCoffeeScriptにはあるのかしらん……と、思ったら、ちょっと微妙ではあるが、ある。

  クラスの中、メソッドの外に「@xxx」を定義しておき、メソッド内からは「@constructor.xxx」でアクセスする。ただし、利用はそのクラスのみに閉じ、継承したクラスからはアクセスできない。今回の場合、その仕様で問題ないが。

  画像の説明

  つうわけで、こんな感じで気持ちよくグルグルと回転している。いや、canvas要素には、ネイティブな回転機能はあるようなのだが、それはそれとして、実装するのが楽しいのだから、これでいいのだ。コードはこんな感じ。

 #---------------------------------------------------------------
 #
 #   回転拡大縮小可能オブジェクト(mixin用)
 #
 class Rotnscl
 
     rotnscl: (pats, n, v, t, w = null, h = null, dx = null, dy = null) ->
         pat = pats[n]
         w ||= pat.width;  dx ||= -(w >> 1)
         h ||= pat.height; dy ||= -(h >> 1)
 
         return(rpat) if(rpat = @constructor.rpats[rsym = "#{n}_#{t}_#{v}_#{w}_#{h}_#{dx}_#{dy}"])
 
         unless(pdat = @constructor.pdats[psym = "#{n}"])
             src_canvas = document.createElement('canvas')
             src_canvas.width  = pat.width
             src_canvas.height = pat.height
             src_context = src_canvas.getContext('2d')
             src_context.drawImage(pat, 0, 0)
             pdat = @constructor.pdats[psym] = src_context.getImageData(0, 0, w, h).data
 
         rot_canvas = document.createElement('canvas')
         rot_canvas.width  = w
         rot_canvas.height = h
         rot_context = rot_canvas.getContext('2d')
 
         rxys = Vec.v2vxy(v, t); hxys = Vec.v2vxy(v + 32 & 0x3F, t); vxys = Vec.v2vxy(v + 48 & 0x3F, t)
         _vx8 =   dx * rxys[1] - dy * rxys[0]  - (dx << 8)
         _vy8 = -(dx * rxys[0] + dy * rxys[1]) - (dy << 8)
         w8 = w << 8; pw = pat.width
         for y in [0...h]
             _hx8 = _vx8; _hy8 = _vy8
             for x in [0...w]
                 _hx8 += hxys[0]; _hy8 += hxys[1]                # 横方向加算
                 continue if(_hx8 < 0 or _hx8 > w8)
                 p = ((_hy8 >> 8) * pw + (_hx8 >> 8)) << 2
                 continue unless(pdat[p] + pdat[p + 1] + pdat[p + 2])    # 黒は透過
                 rot_context.fillStyle = "rgb(#{pdat[p]}, #{pdat[p + 1]}, #{pdat[p + 2]})"
                 rot_context.fillRect(x, y, 1, 1)
             _vx8 += vxys[0]; _vy8 += vxys[1]                    # 縦方向加算
         return(@constructor.rpats[rsym] = rot_canvas)
 
     cache_status: ->
         console.log([@constructor.name, Object.keys(@constructor.pdats).length, Object.keys(@constructor.rpats).length].toString()) if(tsc % 60 == 0)
 
 #---------------------------------------------------------------
 #
 #   回転オブジェクトサンプル
 #
 class Rsample extends mixOf Obj, Rotnscl
 
     Plane.image_src(@pats = [
         'images/rsample0.png'                   #  0:  46 x  46
     ])
     @pdats = {}; @rpats = {}
 
     constructor: (_s, x, y) ->
         super(_s, x, y)
 
     draw: ->
         n = 0                                   # パターンナンバ
         v = (tsc >> 2) % 64                     # 角度
         t = 0                                   # 倍率
         rpat = @rotnscl(@constructor.pats, n, v, t)
         @context.drawImage(rpat, @pos_x, @pos_y)
 
     d0: ->
         @cache_status()

  さて、別にミサイルはグルグル回したかったわけじゃない。意図した動きをプログラミングしていくことにしよう。