昔から車輪の再開発が大好きなので、PHPのフレームワークを作ったり、テンプレートエンジンを作ったり、ゲームエンジンを作ったりしています。
「俺が考えた最強のゲーム」を作る前に「俺が考えた最強のゲームエンジン」を作り始めて、プロジェクトが全部エターなる、というのはよくある話ですよね。
sol.js
昔作作成したゲームエンジンはこちらです。
https://github.com/isdriven/sol.js
自分頑張ったな、と思う反面、今ならこうするな、という部分がたくさん見つかっています。
CrossCode
最近 CrossCode というゲームに感動したんですが、中身が impact.js と NW.js で出来ていたことにショックを受けました。
そこで、「jsでゲームを作成して、公開する」をプロジェクト化しました。
そこはimpact.jsを自分も使うところだ、とは思いますが、これもSAGAでしょう。
Suger.js
sol.js は CanvasとDOMを両方サポートしていましたが、Suger.js は すべてCanvasになります。
まだまだ足りない機能が多すぎるのですが、いったんCanvasに自由に描画する部分が完成したので、公開です。
チュートリアルなどをきれいに付け加えたダウンロードセットなどは配布しない予定ですが、ライセンスはMITです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 |
/** * suger.js * https://sugerfield.club * This is Javascript Game Library. * License:THE MIT LICENSE * Update:2022.12 * Version: 0.0.1 */ Suger = {}; Suger.src = {}; Suger.buffer = []; Suger.actions = []; (function(){ Suger.each = function(l, f){if(l.length){for(let i=0;i<l.length;i++){f(l[i],i)}}else{for(let k in l){f(l[k],k)}}} let __TIMELINE = [] let __TIMELINEPRE = [] let __gameLoop = function(){ let __newline = [] Suger.each( __TIMELINE, function(v,k){ v.frame(); v.count+=1; if( v.valid ){__newline.push(v)} }) __TIMELINE = __newline.concat(__TIMELINEPRE) __TIMELINEPRE = [] setTimeout( __gameLoop, 1000/60 ) } __gameLoop() // 起動しておく let putTL = function(f){ __TIMELINEPRE.push({ count:0,frame:f,valid:true }) } Suger.init = function(id,width,height,f){ window.onload = function(){ let doc = document.getElementById(id) doc.setAttribute("width",width) doc.setAttribute("height",height) doc.style.width=width doc.style.height=height Suger.ctx = doc.getContext('2d') Suger.canvasWidth = width Suger.canvasHeight = height let _loading = f() Suger.each( _loading, function(v,k){ if( v.w ){ v.loaded = false v.dom = new Image() v.dom.onload = function(){v.loaded = true} v.dom.src = v.src Suger.src[v.name] = v }else{ } }) putTL(function(){ let valid = true Suger.each(Suger.src, function(v){ if( !v.loaded ){ valid = false} }) if( valid ){ this.valid = false Suger._main() }else{ console.log("...loading") } }) } } Suger.main = function(f){ Suger._main = f } Suger.put = function(name,x=0,y=0,scale=1.0,alpha=1.0,rotate=0){ if( Suger.src[name] ){ let t = Suger.src[name] let ins = { name: name, src: t.dom, x: x, y: y, z: 0, alpha: alpha, rotate: rotate, scale: scale, xFrames:t.xf, yFrames: t.yf, width: t.w, height: t.h, fWidth: (t.w/t.xf), fHeight: (t.h/t.yf),frame:1, valid: true } Suger.buffer.push(ins) return ins } } Suger.gear = function(f){ let actions = [] let ret = {} actions.push(f) ret.chain = function(f){ actions.push(f) return ret } ret.fire = function(v){ Suger.actions.push({ actions: actions, step: 0, count: 0, valid: true, target: v }) } return ret } Suger.draw = function(v){ let sx = ( v.frame % v.xFrames ) * v.fWidth let sy = ( (v.frame-1) / v.xFrames ) * v.fHeight let w = v.fWidth * v.scale let h = v.fHeight * v.scale Suger.ctx.save() Suger.ctx.globalAlpha = v.alpha Suger.ctx.translate(+v.x, +v.y) Suger.ctx.rotate(v.rotate/180*Math.PI) Suger.ctx.translate(-v.x,-v.y) Suger.ctx.drawImage( v.src, sx, sy, v.fWidth, v.fHeight, v.x-(~~(w/2)), v.y-(~~(h/2)), w, h ); Suger.ctx.restore() } putTL(function(){ let __newActions = [] Suger.each( Suger.actions, function(v){ if( v.actions[v.step](v) === false ){ v.step++ v.count = -1 } v.count++ if( v.actions.length > v.step ){ __newActions.push(v) } }) Suger.actions = __newActions }) putTL( function(){ Suger.ctx.clearRect(0,0,Suger.canvasWidth, Suger.canvasHeight) Suger.buffer.sort(function(a,b){return a.z - b.z}) let __newBuffer = [] Suger.each( Suger.buffer, function(v){ if( v.valid ){ Suger.draw(v) __newBuffer.push(v) } }) Suger.buffer = __newBuffer }) })() |
ちなみに Sol.js は1349 行。 Suger.js は 140 行 です。
おおよそ 10分の1 になりました。
利用方法は以下のようなコードになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
Suger.init("field", 1200, 900, function(){ return [ {name:"girl",src:"src/girl.png",w:100,h:240,xf:1,yf:1} ] }) function initGame(){ let girl = Suger.put("girl",300,200,1.0,0.5,0) let girl2 = Suger.put("girl",200,500,1.2,1.0,0) let girlAct = Suger.gear(function(act){ if( act.count < 100 ){ act.target.rotate+=6 act.target.scale+=0.02 return true } return false }) girlAct.fire(girl2) } Suger.main(function(){ initGame() }) |
Suger.init ではCanvasのID、width、heightを指定し、次の関数で読み込むためのイメージのリストを返します。
すべての読み込みが終了すると、Suger.main で設定された関数が呼ばれます。
mainの中ではまず Suger.put で 持ってきたイメージからインスタンスにし、すべてspriteとしてフレームを指定して描画します。
rotate(回転)、alpha(透明度)、scale(縮尺)、なども指定できます。
さらに Suger.gear でact.countを見て、経過時間でtargetのx,y,rotate,alpha,scaleをコントロールする設計図のインスタンスを作成します。またこのインスタンスはchainで次のアクションを指定できます。
そのインスタンスのfireメソッドで対象に対してアクションを実行できます。
このアクションメソッドは同一のものを何回も適用できます。
コンセプト作り
オレオレライブラリ作成で一番面白いのはコンセプト作りです。
enchant.js などにもみられるように、よくあるライブラリは class … によって基底クラスもしくは基底Entityを継承した設計図を作成し、さらに各機能追加時は基底を継承した新しいクラスを作成し、そのインスタンス化を行い….といった風に、まさにオブジェクト指向なパターンになっていきます。
前回の sol.js はかなりそういった作りになっていたのですが、完全なベースからのヒエラルキーを形作れていないので、結果としてはあまり綺麗に作れていません。
今回は canvasだけしか使わないので、むしろ、そのように作ればきれいかな、とも思いました。
がしかし。
一方で、個人の好みとしては巨大なゴッドクラスが出現しがちなクラスヒエラルキーを作成していくタイプがあまり好きではありません。
こちらは CrossCodeを開発した Radical Fish のブログですが、JSのパフォーマンスについて参考になります。
そういった理由で、今回のライブラリは非常にシンプルな作りになっています。
ここからさらに機能を追加し、いくつかアプリも作成していきます。
TODO
ゲーム作りに必要な機能をこちらに書いていきます。どんどん編集していきます。
・三角関数関連ユーティリティ ある座標への途中座標を求める
・円形衝突判定。円形の衝突判定を行う。
・線分と円の衝突判定。線分と円の衝突判定を行う。
・キーボード操作
・タッチ操作
・コントローラー操作