どうも、もぎーです。
最近iPhoneの電源が付かなくなるという故障に遭遇したのですが、Apple Careのおかげで無償交換してもらえました。
ただ2要素認証系は全滅したので日頃からスマホロストには備えておきましょう。
さて、タイトルにある通りですが、簡単にブラウザで動くゲームを作ってみようと思います。
ただの思いつきです。ゲーム内容はカービィ64のフルーツ拾うやつみたいな感じです。
アイコンの画像は好きな画像を使ってください。
よく素材のお世話になるところを紹介しておきます。
ちなみに今回作るゲームはここで遊べるよ
まずは適当なHTMLファイルにcanvas要素を用意しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <style> #canvas { border: solid; border-color: lightgray; } </style> <script> // ここにどんどん書いていくよ </script> </head> <body> <canvas id="canvas" width="480" height="270"></canvas> </body> </html> |
こんなもんでいいでしょう。
次にゲームを動かすJavascriptを書いていくのですが、以下のような順で進めます。
- ゲームの枠組みであるメインフレームを作る
- ゲームのオブジェクトを作成してcanvasに画像を描画する
- ゲーム的な動きを付けていく
最後にHTMLファイルにペタッと貼り付けて画像を用意すれば良い状態のものを貼り付けておきます。
ゲームの枠組みであるメインフレームを作る
メインフレームとはなんぞやという方のために。
ゲームって60fpsだったり、30fpsだったり一定の間隔で処理が動いています。
60fpsというのは1秒間に60回処理をすることを表しています。
Javascript上で1秒間に60回処理が動くように設定していく = メインフレーム作成ですね。
はい、ペタり
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
var canvas var context const FRAME_PER_SEC = 60 //メインフレームのFPS window.onload = function() { // 初期化処理 initialize() // メインフレームを設定 setInterval(main, 1000 / FRAME_PER_SEC) } var main = function() { console.log("1/60秒ごとにでるよ") } var initialize = function() { canvas = document.getElementById("canvas") context = canvas.getContext('2d') } |
1000ms / 60で1秒を60で割っています。
setIntervalの動きは、mainメソッドを1000ms / 60秒毎に実行するという意味です。
initalizeはゲーム起動時のローディングみたいなものです。
ここではHTMLのcanvasの取得とcontext2Dの取得をしています。
このあと中身を記載していきます。
なんとメインフレームはもう完成です。次にいきましょう。
ゲームのオブジェクトを作成してcanvasに画像を描画する
ここからちょっとは面倒になってきます。
ですが小規模開発なので色々考慮せず実装していきます。
ペタり
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
var objects = {} class Vector { x = 0 y = 0 constructor(x,y) { this.x = x this.y = y } } class GameObject { id // オブジェクトの識別子 pos // オブジェクトの座標 vel // オブジェクトの速度 imageObj // 画像 constructor(posVec, velVec, imageObj) { this.pos = posVec this.vel = velVec this.imageObj = imageObj this.id = new Date().getTime().toString(16) objects[this.id] = this // 全てのゲームオブジェクトを管理するために設定 } } |
このゲームオブジェクトを生成して、メインフレームの処理毎にobjects内のゲームオブジェクトを更新していくよ。
Vectorはゲームを作る際は必ず付き纏うやつです。
座標・向き・長さなど色々な情報を表現するのに使用されます。
zとか追加すれば3D用ですね。
次はcanvasへの描画。
1 2 3 4 5 6 7 8 9 |
var draw = function() { // canvasをまっさらにしてるよ context.clearRect(0, 0, canvas.width, canvas.height) // GameObject達をcanvasに描画するよ Object.keys(objects).forEach(key => { context.drawImage(objects[key].imageObj, objects[key].pos.x, objects[key].pos.y, IMAGE_SIZE, IMAGE_SIZE) }); } |
Object.keys(objects).reverse().forEachで監視対象のGameObjectを描画していくよ。
context.drawImage(Imageクラス, x座標, y座標, xサイズ, yサイズ)となっています。
この段階で描画する準備は整いました。GameObjectをnewすれば描画されます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
var player const IMAGE_SIZE = 32 // 使用画像の描画サイズ var initialize = function() { counter = 0 canvas = document.getElementById("canvas") context = canvas.getContext('2d') context.font = "12px Arial" bananaImage = new Image() bananaImage.src = "./banana.png" playerImage = new Image() playerImage.src = "./monkey.png" player = new GameObject( new Vector(canvas.width / 2 - IMAGE_SIZE / 2, canvas.height - IMAGE_SIZE*1.5), new Vector(0, 0), playerImage) } |
initializeに肉付けしました。
Imageクラスで画像を用意して、GameObjectのコンストラクタに渡しています。
playerは自分で操作するキャラクタです。
そう言った理由もありInitializeで生成します。
new Vector(canvas.width / 2 – IMAGE_SIZE / 2, canvas.height – IMAGE_SIZE*1.5)
ここに関してはx軸をcanvasの中央、y軸をcanvasの一番したより少し上くらいで表示します。
ここまで来れば、playerImage.srcに設定した画像がcanvasに表示されるかと思います。
順番に紹介するために結構散らかった書き方になっているのでここらでいったんまとめましょう。
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 |
var canvas var context var bananaImage var playerImage var player var objects = {} const FRAME_PER_SEC = 60 //メインフレームのFPS const IMAGE_SIZE = 32 // 使用画像の描画サイズ const PLAYER_VELOCITY = 5 // プレイヤー移動速度 const SCORE_RISE_POINT = 100 // スコアの上げ幅 class Vector { x = 0 y = 0 constructor(x,y) { this.x = x this.y = y } } class GameObject { id // オブジェクトの識別子 pos // オブジェクトの座標 vel // オブジェクトの速度 imageObj // 画像 constructor(posVec, velVec, imageObj) { this.pos = posVec this.vel = velVec this.imageObj = imageObj this.id = new Date().getTime().toString(16) objects[this.id] = this } } window.onload = function() { // 初期化処理 initialize() // メインフレームを設定 setInterval(main, 1000 / FRAME_PER_SEC)//30.0) } var initialize = function() { canvas = document.getElementById("canvas") context = canvas.getContext('2d') context.font = "12px Arial" bananaImage = new Image() bananaImage.src = "./banana.png" playerImage = new Image() playerImage.src = "./monkey.png" player = new GameObject( new Vector(canvas.width / 2 - IMAGE_SIZE / 2, canvas.height - IMAGE_SIZE*1.5), new Vector(0, 0), playerImage) } var main = function() { draw() } var draw = function() { context.clearRect(0, 0, canvas.width, canvas.height) // GameObject Object.keys(objects).forEach(key => { if (key === player.id) return context.drawImage(objects[key].imageObj, objects[key].pos.x, objects[key].pos.y, IMAGE_SIZE, IMAGE_SIZE) }); context.drawImage(player.imageObj, player.pos.x, player.pos.y, IMAGE_SIZE, IMAGE_SIZE) } |
これでゲームのオブジェクトを作成してcanvasに画像を描画することができたかと思います。
ゲーム的な動きを付けていく
基本的なことができたらここからは自由です。
今回取り上げるのは、以下の要素です。
- 一定間隔毎に上からバナナが降ってくる
- サルを操作してバナナを拾う
- UIとしてプレイ時間とスコアを表示する
画面イメージは以下です。
質素ですが、ゲームエンジンとかを使わないと最初はこんなもんです。
(ぶっちゃけ最初からUnityとか触った方がメリットしかないと思うよ)
記事が長くなってきているので一気に載せて後から説明します。
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 |
var canvas var context var bananaImage var playerImage var counter = 0 var playTime = 0 var score = 0 var player var objects = {} const FRAME_PER_SEC = 60 //メインフレームのFPS const IMAGE_SIZE = 32 // 使用画像の描画サイズ const PLAYER_VELOCITY = 5 // プレイヤー移動速度 const SCORE_RISE_POINT = 100 // スコアの上げ幅 class Vector { x = 0 y = 0 constructor(x,y) { this.x = x this.y = y } } class GameObject { id // オブジェクトの識別子 pos // オブジェクトの座標 vel // オブジェクトの速度 imageObj // 画像 constructor(posVec, velVec, imageObj) { this.pos = posVec this.vel = velVec this.imageObj = imageObj this.id = new Date().getTime().toString(16) objects[this.id] = this } destroy() { delete objects[this.id] } move() { this.pos.x += this.vel.x this.pos.y += this.vel.y if (this.pos.y > canvas.height - IMAGE_SIZE*1.5) { this.destroy() } } } window.onload = function() { // 初期化処理 initialize() // メインフレームを設定 setInterval(main, 1000 / FRAME_PER_SEC)//30.0) } var initialize = function() { canvas = document.getElementById("canvas") context = canvas.getContext('2d') context.font = "12px Arial" bananaImage = new Image() bananaImage.src = "./banana.png" playerImage = new Image() playerImage.src = "./monkey.png" player = new GameObject( new Vector(canvas.width / 2 - IMAGE_SIZE / 2, canvas.height - IMAGE_SIZE*1.5), new Vector(0, 0), playerImage) // プレイヤー操作を設定 // メインフレームに合わせて操作を受け付けるべきだが今は簡単さを重視 document.addEventListener('keydown', function(e) { switch(e.key) { case 'ArrowRight': player.pos.x += PLAYER_VELOCITY break; case 'ArrowLeft': player.pos.x -= PLAYER_VELOCITY break; } }) } var main = function() { checkCollisionDropObject() Object.keys(objects).forEach(key => { objects[key].move() }) createDropObject() playTime += 1 draw() debugConsole() } var draw = function() { context.clearRect(0, 0, canvas.width, canvas.height) // GameObject Object.keys(objects).forEach(key => { if (key === player.id) return context.drawImage(objects[key].imageObj, objects[key].pos.x, objects[key].pos.y, IMAGE_SIZE, IMAGE_SIZE) }); context.drawImage(player.imageObj, player.pos.x, player.pos.y, IMAGE_SIZE, IMAGE_SIZE) // UI context.fillText("プレイ時間:" + Math.floor(playTime / FRAME_PER_SEC).toString(), 5, 5 + 12) context.fillText("スコア:" + score, canvas.width - 80 - 5, 5 + 12) } // いい感じに一定間隔でバナナを生成するメソッド var createDropObject = function() { if (playTime / 20 % 2 === 0) { new GameObject( new Vector(Math.random() * (canvas.width - IMAGE_SIZE), 0), new Vector(0, 2), bananaImage ) } } var checkCollisionDropObject = function() { Object.keys(objects).forEach(key => { if (key === player.id) return // 当たり判定 if (player.pos.x <= objects[key].pos.x + IMAGE_SIZE // 左端の判定 && player.pos.x + IMAGE_SIZE >= objects[key].pos.x // 右端の判定 && player.pos.y <= objects[key].pos.y + IMAGE_SIZE) { // 上端の判定 objects[key].destroy() score += SCORE_RISE_POINT } }) } |
一定間隔毎に上からバナナが降ってくる
これは
- メインフレームでcreateDropObjectを呼び出してバナナを生成
- メインフレームで各バナナが重力で落下するように速度を処理(move()の部分)
- バナナが地面についた時点でそのバナナは消える(move()内でdestroyを読んでいる部分)
このように実現しています。
この辺りはどのタイミングでどこにオブジェクトを作るか、どのように動かすか、動いたらどうなるかの部分ですね。
サルを操作してバナナを拾う
これは
- キー入力を受けてサルの座標を操作
- バナナにコリジョン(衝突)したらバナナを消してスコアを獲得
ここでついにユーザーによる操作が可能になりました。
当たり判定については少しでもアクション系のゲームを作ることになったら必ず付き纏うものです。
今回は画像の座標とサイズから、サルとバナナの画像が重なった場合衝突と判定しています。
UIとしてプレイ時間とスコアを表示する
これは
・メインフレームで経過時間をカウント
・描画処理でUIを描画
これもゲームではほとんど必要になってくるUI部分ですね。
UIはほとんどの場合ゲームないオブジェクトよりは上のレイヤーで描画をしたいので、基本的には最後に描画するようにしましょう。
ポーズ画面のような動きを作るときはポーズ用の表示の方がレイヤーが上になることが多いです。
今回はここまでとします。
すごく簡単なゲームもどきですが、実際に動きのあるものだとそれなりに楽しめるかと思います。
最初に貼ったHTMLファイルのscriptタグ内に最後に貼ったJavascriptを貼り付けば動くようになっていますので是非お試しあれ。
FirebaseあたりでHostingとかしようと思ったのですがなぜかすぐできなかったので許してください。
と諦めようとしたところfirebase logoutからのfirebase loginでtoken問題が解決したのでHostingできました。遊んでいってくれてもいいんですよ。