HTMLのバージョン5からcanvasという要素が追加されました。
このcanvasはブラウザ上にグラフィックの描画エリアを作ることができます。
この描画エリアにはJavaScriptを使って様々なグラフィックを表示することができます。
なのでもちろんゲームも作ることができるわけです。
そしてこのcanvasを利用して作られたゲームのことをHTML5ゲームと呼んだりします。
JavaScriptを使った開発ではライブラリを使うのが一般的で、もちろんゲームに使えるライブラリも色々あります。
ただ、今回はライブラリを使わない素のJavaScriptだけで作ります。
ということでめっちゃシンプルなシューティングゲームもどきを作ってみました。
簡単なフレーム更新プログラム
まずは簡単なフレーム更新プログラムを作ってみました。
ソースコードはこんな感じです。
var canvas, ctx; var x = 0, y = 0, vx = 1, vy = 1; //初期処理 function init(){ canvas = document.getElementById("canvas"); canvas.width = 640; canvas.height = 900; ctx = canvas.getContext("2d");//コンテキスト requestAnimationFrame(update);//フレーム処理のお知らせを送る } //四角を描く function drawRect(x, y, w, h){ ctx.fillStyle = "white"; ctx.fillRect(x, y, w, h); } //画面描画処理 function render(){ ctx.clearRect(0, 0, canvas.width, canvas.height);//画面をクリア(前の画面描画を削除) x += vx; y += vy; if(x >= canvas.width - 64){ vx = -1; }else if(x <= 0){ vx = 1; } if(y >= canvas.height - 64){ vy = -1; }else if(y <= 0){ vy = 1; } drawRect(x, y, 64, 64); } //更新処理 function update(){ render(); requestAnimationFrame(update);//フレーム処理のお知らせを送る } //開始 window.onload = function(){ init(); }
このなかで大事なのはrequestAnimationFrameというメソッドです。
これはおよそ60FPSになるような一定の間隔で引数に渡した関数を呼び出してくれるようです。
ただし、requestAnimationFrameは一回こっきりしか働いてくれないようで、ずっと更新処理をし続けるには毎回requestAnimationFrameを呼ぶ必要があるようです。
画面が更新されていても何も表示されていなければわからないので四角形を表示させて移動させてみました。DVDプレーヤーのアレみたいなやつを作ってみました。
角に行ったら盛り上がってください(外国で流行っているらしいです)。
仮のキャラを表示して作ってみる
とりあえず画面の更新処理さえできてしまえばもうゲームは出来るはずです。
ってことで作ってみたのがこちらです → サンプルプログラムを見る
かなり簡素ではありますが一応シューティングゲームになってます。
プログラムはこんな感じでやってみました。
var SCREEN_WIDTH = 640;//画面幅 var SCREEN_HEIGHT = 900;//画面高さ var core, player, bullet, enemy; class Chara{//キャラクタークラス constructor(w, h){ this.x = 0; this.y = 0; this.width = w; this.height = h; } render(){ drawRect(core.ctx, this.x, this.y, this.width, this.height); } } class Bullet extends Chara{//弾クラス constructor(w, h){ super(w, h); this.y = -this.height; } move(){ this.y -= 10; if(this.y < -this.height){ this.x = player.x + (player.width - this.width) / 2; this.y = player.y; } } } class Player extends Chara{//プレイヤークラス constructor(w, h){ super(w, h); this.x = 300; this.y = 500; } } class Enemy extends Chara{//敵クラス constructor(w, h){ super(w, h); this.x = Math.floor(Math.random() * (SCREEN_WIDTH - this.width)); this.y = -this.height; } move(){ this.y += 2; if(this.y > SCREEN_HEIGHT){ this.x = Math.floor(Math.random() * (SCREEN_WIDTH - this.width)); this.y = -this.height; } } } class Core{//ゲーム基幹クラス constructor(){ this.canvas = document.getElementById("canvas"); this.canvas.width = SCREEN_WIDTH; this.canvas.height = SCREEN_HEIGHT; this.ctx = this.canvas.getContext("2d");//コンテキスト this.canvas.addEventListener("mousedown", (e) => {//マウスダウン this.isMouseDown = true; player.px = e.x; player.py = e.y; }); this.canvas.addEventListener("mouseup", (e) => {//マウスアップ this.isMouseDown = false; }); this.canvas.addEventListener("mousemove", (e) => {//マウス移動 if(!this.isMouseDown)return; player.x += e.x - player.px; player.y += e.y - player.py; player.px = e.x; player.py = e.y; }); requestAnimationFrame(()=>{this.update();});//フレーム処理のお知らせを送る } update(){//更新処理 requestAnimationFrame(()=>{this.update();});//フレーム処理のお知らせを送る this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);//画面をクリア(前の画面描画を削除) this.enterframe();//フレーム処理 this.render();//描画処理 } render(){//描画処理 enemy.render(); player.render(); bullet.render(); } enterframe(){//フレーム処理 bullet.move(); enemy.move(); var r1 = bullet.width / 2; var r2 = enemy.width / 2; if(circleCollision(bullet.x + r1, bullet.y + r1, r1, enemy.x + r2, enemy.y + r2, r2)){ enemy.y = SCREEN_HEIGHT; bullet.y = -100; } } } function drawRect(ctx, x, y, w, h){//四角を描く ctx.fillStyle = "white"; ctx.fillRect(x, y, w, h); } function circleCollision(x1, y1, r1, x2, y2, r2){//当たり判定 if((x1-x2) * (x1-x2) + (y1-y2) * (y1-y2) <= (r1+r2) * (r1+r2)){ return true; } return false; } //開始 window.onload = function(){ core = new Core(); enemy = new Enemy(64, 64); player = new Player(64, 64); bullet = new Bullet(16, 16); }
ゲームにするためには操作が必要なのでmousedown, mouseup, mousemoveを使って操作できるようにしました。
ただ、これはマウスでの操作にしか反応してくれません。スマホなどのタッチでの操作はまた別に作る必要があります。
一応シューティングゲームなのでちゃんと当たり判定も付けています。
ゲームで使う当たり判定の作り方はこちらの記事にまとめています。
画像を読み込んで表示する
最後にキャラクターの画像を表示させてみます→サンプルを見る
画像はプレイヤー、敵、弾の3つ用意します。
大きさはプレイヤー、敵が64×64、弾が16×16です。それらをimagesフォルダに入れてください。
画像の読み込み処理
まずは使う画像をASSETSオブジェクトにまとめて入れておきます。
それぞれの画像の名前とファイルの場所を入れます。
var ASSETS = { 'img_player': 'images/player.png', 'img_enemy': 'images/enemy.png', 'img_bullet': 'images/bullet.png', };
画像の読み込みなんですが、まず画像を扱うにはImageオブジェクトを作成してそこに画像を読み込んで使います。
var image = new Image(); image.src = '画像ファイルの場所'; image.onload = function() {//読み込んだよ }
基本、上記のような感じで書きます。
onloadメソッドは画像の読み込みが完了したら発生するイベントです。大きな画像だったりすると読み込むのに時間がかかったりするので必要な処理は読み込みが終わってからする必要があります。
ただ、ゲームだとたくさん画像を扱うので全部読み込んだかどうかの確認が必要です。
なので、Coreクラスに以下のような処理をつけました。引数のdataはASSETSです。画像はすべてCoreクラスのassetsオブジェクトに読み込みます。
preload(data){ var length = Object.keys(data).length; var count = 0; for(let key in data) { this.assets[key] = new Image(); this.assets[key].src = data[key]; this.assets[key].onload = ()=>{ if(++count == length){//全部読み込んだ this.onload();//読み込み完了後の処理 } } } }
画像の枚数をカウントしておいて、読み込みの完了した数が画像の枚数と同じになればCoreクラスのonloadメソッドを実行します。
それを追加したCoreクラスがこんな感じです。
class Core{//ゲーム基幹クラス constructor(){ this.canvas = document.getElementById("canvas"); this.canvas.width = SCREEN_WIDTH; this.canvas.height = SCREEN_HEIGHT; this.ctx = this.canvas.getContext("2d");//コンテキスト this.assets = [];//アセット保存用配列 this.canvas.addEventListener("mousedown", (e) => {//マウスダウン this.isMouseDown = true; player.px = e.x; player.py = e.y; }); this.canvas.addEventListener("mouseup", (e) => {//マウスアップ this.isMouseDown = false; }); this.canvas.addEventListener("mousemove", (e) => {//マウス移動 if(!this.isMouseDown)return; player.x += e.x - player.px; player.y += e.y - player.py; player.px = e.x; player.py = e.y; }); } //画像のロード preload(data){ var length = Object.keys(data).length; var count = 0; for(let key in data) { this.assets[key] = new Image(); this.assets[key].src = data[key]; this.assets[key].onload = ()=>{ if(++count == length){//全部読み込んだ this.onload();//読み込み完了後の処理 } } } } update(){//更新処理 requestAnimationFrame(()=>{this.update();});//フレーム処理のお知らせを送る this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);//画面をクリア(前の画面描画を削除) this.enterframe();//フレーム処理 this.render();//描画処理 } render(){//描画処理 enemy.render(); player.render(); bullet.render(); } enterframe(){//フレーム処理 bullet.move(); enemy.move(); var r1 = bullet.width / 2; var r2 = enemy.width / 2; if(circleCollision(bullet.x + r1, bullet.y + r1, r1, enemy.x + r2, enemy.y + r2, r2)){ enemy.y = SCREEN_HEIGHT; bullet.y = -100; } } }
読み込み完了後にゲームが始動
画像の読み込みが完了したらゲームプログラムが動き始めるようにします。
core.onloadメソッドに読み込み完了後の処理を記述します。ここで初めてrequestAnimationFrameを使います。
この後は自動で更新処理が行われるようになります。
//開始 window.onload = function(){ core = new Core(); core.preload(ASSETS); core.onload = function(){ enemy = new Enemy(64, 64); player = new Player(64, 64); bullet = new Bullet(16, 16); requestAnimationFrame(()=>{core.update();});//フレーム処理のお知らせを送る } }
画像を描画する
Charaクラスのレンダーメソッドに画像を描画するdrawImageメソッドを記述します。
class Chara{//キャラクタークラス constructor(w, h){ this.x = 0; this.y = 0; this.width = w; this.height = h; } render(){ core.ctx.drawImage(this.image, 0, 0, this.width, this.height, this.x, this.y, this.width, this.height); } }
キャラクターはすべてこのCharaクラスを継承しているのでこの一か所変更するだけでOKです。
これで画像を使ったゲームになります。