SVX日記
2022-08-04(Thu) CoffeeScriptにて回転機能をMixinにより実装す
意外と飽きずにオリジナルのシューティングの製作をポチポチと進めているのだが、考えれば考えるほど、既存のシューティングに対する気付きが増えていく。
例えば、R-TYPEは初見でも1面はクリアさせたい作りなんだな、とか、ダライアスはボス以外の作りは適当だな、とかである。反面、ゼビウスは「最初は偵察機が登場する」とか「有人機は自殺的攻撃をしない」とか考えてあることに驚く。そうだよな。それは世界感を感じさせるために有効だよな。
今回、自分は「ストーリ」を強く感じさせるゲームにしたいので、単に思いつきで敵を作るのではなく、それなりの理由に基づいて登場、行動させたい。そうなると、最初は偵察機だし、その後はミサイル攻撃かな、とか思うわけだ。
で、ミサイルを作ったのだが、誘導したくなってしまい、斜めに飛ばしたくなってしまった。すると、回転機能が必要になる。先日のテトランはプリレンダだったが、汎用性を考えればリアルタイムレンダを実装するべきだ。しかもキャッシュ付きで、できるだけ軽く。
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クラスに実装してしまう方法も考えられるが、それは気が進まない。必要に応じて回転機能が追加できるのが望ましい。
#---------------------------------------------------------------
#
# クラスの 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 # 敵+回転拡大縮小機能
次は、回転パターンのキャッシュについて考える。処理負荷的にリアルタイムレンダは可能な見通しだが、毎回、回転パターンを作るのは無駄なので、一度作った回転パターンは残しておき、二度目からは勝手にそれを使うようにしたい。つまりはキャッシュだ。
キャッシュはどう実装されるべきか、といえば、そりゃオブジェクトタイプ、パターン、角度毎に残しておくべきで、個体(インスタンス)毎に行うべきではない。つまり、クラス変数に残しておくべきだ。クラス変数は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()