Canvas に触ってまだ 1 か月以上ですが、ゲームのプロセスを完全に実装するのは初めてですが、収穫は非常に大きいです。
シューティングゲームのスクリーンショット
まずデモにアクセスしてください: https://littleyljy.github.io/demo/shootgame/
ゲームのルールプレイヤーは航空機を制御して弾を発射し、移動するモンスターをすべて破壊する必要があります。モンスターが最下位に移動するとゲームは失敗します。
ゲームはいくつかのシーンに分かれています。
シーンの切り替えを実現するには、実際には、最初にすべてのシーンに対して display: none を実行し、次に js を通じて data-status を制御して start、playing、failed、success、all-success、stop を制御し、対応するシーンの display: ブロックを実装する必要があります。
HTMLとCSSは以下の通りです。
<div id=game data-status=start> <div class=game-panel> <section class=game-intro game-ui> <h1 class=section-title>シューティング ゲーム</h1> <p class=game- desc>これは中毒性のあるシューティング ゲームです。← と → を使用して飛行機を操作し、スペースを使用して射撃し、Enter を使用してゲームを一時停止します。一緒に宇宙怪獣を倒しましょう! </p> <p class=game-level>現在のレベル: 1</p> <button class=js-play button>ゲームを開始</button> </section> <section class=game-failed game-ui> <h1 class=section-title>ゲームオーバー</h1> <p class=game-info-text>最終スコア: <span class=score></span></p> <button class=js-replay button>もう一度開始</ボタン> </section> <section class=game-success game-ui> <h1 class=section-title>ゲームの成功</h1> <p class=game-next-level game-info-text></p> <button class=js-next button>ゲームを続行</button> </section> <section class=game-all-success game-ui> <h1 class=section-title>正常に成功しました</h1> <p class=game-次のレベルgame-info-text>あなたはすべてのモンスターの攻撃を防御することに成功しました。 </p> <button class=js-replay button>もう一度プレイ</button> </section> <section class=game-stop game-ui> <h1 class=section-title>ゲーム一時停止</h1> < button class=js-stop button>ゲーム続行</button> </section> </div> <div class=game-info game-ui> <span class=title>スコア:</span> <span class=score > </スパン> </div> <canvas id=canvas width=700 height=600> <!-- アニメーションお絵かきボード --> </canvas> </div>
#game{ 幅: 700px; 高さ: 600px; 位置: 左: 50%; マージン: 0 0 0 -350px; );}.game-ui{ 表示: パディング: 55 ピクセル;ボーダーボックス; 高さ: 100%;} [data-status=start] .game-intro { 表示: ブロック; パディングトップ: 180px; 背景: url(./img/bg.png) 繰り返しなし 430px 180px;背景サイズ: 200px;}[data-status=playing] .game-info {表示: ブロック; 位置: 絶対; left:0;padding:20px;}[data-status=failed] .game-failed,[data-status=success] .game-success,[data-status=all-success] .game-all-success,[ data-status=stop] .game-stop{ 表示: ブロックトップ: 180px; url(./img/bg-end.png) 繰り返しなし 380px 190px; }オブジェクト指向
ゲーム全体では、モンスター(Enemy)、飛行機(Plane)、弾丸(Bullet)をオブジェクトとして扱うことができるほか、ゲームロジックを制御する構成オブジェクト(CONFIG)やゲームオブジェクト(GAME)を扱うことができます。
ゲーム関連の設定/** * ゲーム関連の設定* @type {Object} */var CONFIG = { status: 'start', // ゲームはデフォルトで開始レベルとして開始します: 1, // ゲームのデフォルトレベル totalLevel: 6, //合計 6 オフ numPerLine: 7, // 1 行あたりのゲームのデフォルトのモンスター数 CanvasPadding: 30, // デフォルトのキャンバス間隔 BulletSize: 10, // デフォルトの箇条書きの長さ BulletSpeed: 10, //デフォルトの弾丸の移動速度emoneSpeed: 2, // デフォルトの敵の移動距離emoneSize: 50, // デフォルトの敵のサイズemoneGap: 10, // デフォルトの敵間の距離emoneIcon: './img/enemy.png', // の画像モンスターの敵BoomIcon: './img/boom.png', // モンスターの死の画像emoneDirection: 'right', //デフォルトの敵は開始時に右に移動します planSpeed: 5, // 飛行機が各ステップで移動するデフォルトの距離 planSize: { width: 60, height: 100 }, // 飛行機のデフォルトのサイズ、planeIcon: '。 /img/plane.png' };親クラスを定義する
モンスター(Enemy)、飛行機(Plane)、弾丸(Bullet)はすべて同じx、y、size、speed属性、move()メソッドを持つため、親クラスのElementを定義し、親クラスを継承して実装することができます。サブクラス 。
/*親クラス: xy 速度 move()draw()*/var 要素 = function (opts) { this.opts = opts {} //座標、サイズ、速度を設定します this.x = opts.x; this.y = opts.y; this.size = opts.speed;};Element.prototype.move = function (x, y) { var addX = x var; addY = y || 0; this.x += addX; this.y += addY;};//プロトタイプ関数を継承する関数 extendPrototype(subType, superType) { var proto = Object.create(superType.prototype);プロト .constructor = サブタイプ.プロトタイプ = プロト;}
move(x, y) メソッドは、渡された (x, y) 値に基づいてそれ自体をスタックします。
モンスターの定義モンスターには、モンスターのステータス、画像、爆発状態の継続時間を制御するboomCount、draw()、down()、direction()、booming()メソッドなどの固有の属性が含まれます。
/*Enemy*/var Enemy = function (opts) { this.opts = opts || {} // 親クラスの属性を呼び出します Element.call(this, opts); // 特殊な属性のステータスとイメージ this.status = 'normal';//通常、ブーミング、noomed this.enemileIcon = opts.emoneIcon; this.enemeneBoomIcon = opts.emoneBoomIcon; 0;};//要素を継承しますメソッド継承Prototype(Enemy, Element);//メソッド: 敵を描画 Enemy.prototype.draw = function () { if (this.emoneIcon && this.emoneBoomIcon) { switch (this.status) ) { '通常' の場合: var 敵 Icon = new Image() = this.emoneIcon(敵 Icon, this.x, this.y, this.size, this.size); ブレーク; ケース 'booming': var evilBoomIcon = new Image(); this.y, this.size, this.size); ブレーク; ケース 'boomed': ctx.clearRect(this.x, this.y, this.size, this.size); Break; デフォルト: Break; } } return this;};//メソッド: down Enemy.prototype.down = function () { this.move(0, this.size); ;//メソッド: 左または右に移動 Enemy.prototype.direction = function (direction) { if (direction === 'right') { this.move(this.speed, 0) }; this.move(-this.speed, 0); } return this;};// メソッド: 敵が爆発する Enemy.prototype.booming = function () { this.status = 'this.boomCount += 1; (this.boomCount > 4) { this.status = 'boomed' } これを返します;}
Bullet には fly() メソッドとdraw() メソッドがあります。
/*Bullet*/var Bullet = function (opts) { this.opts = opts || {}; Element.call(this, opts);};inheritPrototype(Bullet, Element);//メソッド: 弾丸を飛ばす.prototype.fly = function () { this.move(0, -this.speed); return this;};//メソッド: 弾丸を描画します Bullet.prototype.draw = function () { ctx.beginPath(); ctx.ストロークスタイル = '#fff'; ctx.moveTo(this.x, this.y - CONFIG.bulletSize); ; ctx.ストローク(); を返します。
航空機オブジェクトには、ステータス、幅と高さ、イメージ、横座標の最大値と最小値、およびメソッド haveHit()、draw()、direction()、shoot()、およびdrawBullets() などの固有の属性が含まれています。
/*Plane*/var Plane = function (opts) { this.opts = opts || Element.call(this, opts); // 固有の属性のステータスとイメージ this.status = 'normal'; = opts.width; this.height = opts.height; this.minX = opts.maxX; //箇条書き関連 this.bullets = []; this.bulletSpeed || CONFIG.bulletSpeed; this.bulletSize = opts.bulletSize;};// 要素メソッドを継承します。 (Plane, Element) ;//メソッド: 弾丸がターゲットに当たる Plane.prototype.hasHit = function (enemies) { var Bullets = this.bullets; for (var i = Bullets.length - 1; i >= 0; i--) { var Bullet = Bullets[i]; && (bullet.x) < (敵.x + 敵.サイズ)); var isHitPosY = (敵.y < 弾丸.y) && (弾丸.y < (敵.y + 敵.サイズ)); (isHitPosX && isHitPosY) { this.bullets.splice(i, 1); return true; } } return false;};//メソッド: 平面を描画します Plane.prototype.draw = function () { this.drawBullets(); var planIcon = new Image(); planIcon.src = this.planeIcon(planeIcon, this.x, this.y, this.width, this.height); return this;};//メソッド: 平面の方向 Plane.prototype.direction = function (direction) { varspeed = this.speed if (direction === 'left') ) { プレーンスピード = this.x < this.minX ? 0 : -speed; } else { プレーンスピード = this.x > this.maxX 0 : 速度; console.log('planeSpeed:', planSpeed); console.log('this.x:', this.x); console.log('this.minX:', this.minX); .maxX:', this.maxX); this.move(planeSpeed, 0); return this;//便利なチェーン呼び出し};//メソッド: 弾丸を発射する Plane.prototype.shoot = function () { var BulletPosX = this.x + this.width / 2; this.bullets.push(new Bullet({ x: bulletPosX, y: this.y, size: this.bulletSize, 速度: this.bulletSpeed }); this; };//メソッド: 弾丸を描画 Plane.prototype.drawBullets = function () { var Bullets = this.bullets var i = bullets; (i--) { var Bullet = Bullets[i]; Bullet.fly(); if (bullet.y <= 0) { Bullet.splice(i, 1) }};
キーボード イベントには次の状態があります。
左ボタン (keyCode=37) と右ボタン (keyCode=39) を押している間 (キーダウン)、機体は動き続ける必要があり、キーアップを放しても動きません。スペース (keyCode=32) または上矢印キー (keyCode=38) を押すと (keydown)、弾が発射され、放すと keyup が発射を停止します。また、Enter キー (keyCode=13) を押してゲームを一時停止します。したがって、onkeydown と onkeyup がキーを押しているか離しているかを監視する KeyBoard オブジェクトを定義する必要があります。
左右のキーは矛盾しているため、安全を期すために、左のキーを押すときは右のキーを false に設定する必要があります。右クリックも同様です。
//キーボードイベント var KeyBoard = function () { document.onkeydown = this.keydown.bind(this); document.onkeyup = this.keyup.bind(this);}; //KeyBoard オブジェクト KeyBoard.prototype = { pressedLeft: false、pressedRight: false、pressedUp: false、holdLeft: false、holdRight: false、pressedSpace: false、pressedEnter: false、keydown: function (e) { var key = e.keyCode; スイッチ (キー) { case 32://スペース - 弾丸を発射します this.pressedSpace = true; case 37://左矢印キー this.pressedLeft = true; false; this.heldRight = false; case 38://上矢印キー this.pressedUp = true; case 39://右矢印キー this.pressedLeft = false; = false; this.pressedRight = true; this.heldRight = true; case 13://Enter キー - ゲームを一時停止します this.pressedEnter = true; .keyCode; ケース 32: this.pressedSpace = false; ケース 38: this.pressedUp = false; ケース 39: this.pressedRight = false; ケース 13: this.pressedEnter = false;ゲームロジック
ゲーム オブジェクト (GAME) には、init (初期化)、bindEvent (ボタンのバインド)、setStatus (ゲーム ステータスの更新)、play (ゲーム中)、stop (一時停止)、end (終了) などを含むゲーム全体のロジックが含まれています。 ., in これでは説明が拡張されません。モンスターの生成やゲーム要素の描画などの機能も搭載されています。
//ゲームオブジェクト全体 var GAME = { //一連の論理関数 // ゲーム要素関数}1. 初期化
初期化関数は主に、機体の初期座標、機体の移動範囲、モンスターの移動範囲を定義し、スコア、モンスター配列を初期化し、KeyBoard オブジェクトを作成し、1 回だけ実行します。
/** * 初期化関数、この関数は 1 回だけ実行されます * @param {object} opts * @return {[type]} [description] */init: function (opts) { //Set opts var opts = Object.assign ( {}, opts, CONFIG);//すべてのパラメータを結合します this.opts = opts; this.status = 'start' // 航空機オブジェクトの初期座標を計算します this.planePosX = CanvasWidth / 2 - opts.planeSize.width; this.planePosY = CanvasHeight - opts.planeSize.height - opts.canvasPadding; // 平面制限座標 this.planeMinX = opts.canvasPadding this.planeMaxX = CanvasWidth - opts.canvasPadding; width; //敵の移動範囲を計算する this.emoneMinX = opts.canvasPadding; CanvasWidth - opts.canvasPadding - opts.enemiesSize; // スコアは 0 に設定されます。 this.enemies = []; this.keyBoard = new KeyBoard(); );2.バインドボタンイベント
いくつかのゲーム シーンには、ゲームを開始する (playBtn)、再起動する (replayBtn)、ゲームの次のレベル (nextBtn)、ゲームを一時停止して継続する (stopBtn) ためのボタンが含まれているためです。ボタンごとに異なるイベントを実行する必要があります。
そもそも var self = this を定義する理由は this の使用法です。 bindEvent 関数では、これは GAME オブジェクトを指しますが、playBtn.onclick = function () {}; では、これは明らかに私たちが望んでいることではありません。playBtn には play() イベントしかないからです。 GAMEオブジェクトにはそれがあります。したがって、GAME オブジェクトを変数 self に割り当てる必要があり、その後 playBtn.onclick = function () {}; で play() イベントを呼び出すことができます。
なお、replayBtn ボタンは失敗シナリオとクリアシナリオの両方に表示されるため、取得されるのはすべての .js-replay のコレクションです。次に、forEach は各replayBtn ボタンを反復処理し、レベルとスコアをリセットし、play() イベントを呼び出します。
bindEvent: function () { var self = this; var playBtn = document.querySelector('.js-play'); var restartBtn = document.querySelectorAll('.js-replay'); js-next'); var stopBtn = document.querySelector('.js-stop');ゲーム開始ボタンのバインディング playBtn.onclick = function () { self.play() } //ゲーム開始ボタンのバインディング restartBtn.forEach(function (e) { e.onclick = function () { self.opts. level = 1 ; self.play(); self.score = 0;次のレベルのゲーム ボタン バインディング nextBtn.onclick = function () { self.opts.level += 1; self.play() }; // ゲームを一時停止してボタン バインディング stopBtn.onclick = function (); ('プレイ中');3. 航空機を生成する
createPlane: function () { var opts = this.opts; this.plane = new Plane({ x: this.planePosX, y: this.planePosY, width: opts.planeSize.width, height: opts.planeSize.height, minX : this.planeMinX、速度: opts.planeSpeed、maxX: this.planeMaxX、planeIcon: opts.planeIcon });}4. モンスターのグループを生成する
モンスターはグループで出現し、各レベルのモンスターの数も異なるため、2 つの for ループの機能は、モンスターの行を生成し、レベルの数に応じてモンスターのレベル行を増やすことです。または、モンスターの速度を上げて(速度:速度 + i、)、各レベルの難易度を上げます。
//敵を生成します createEnemy: function (emoneType) { var opts = this.opts; var neighbors = this.enemies = opts.numPerLine; .emoneGap; var size = opts.emoneSize; //敵のレベルごとに行を追加 for (var i = 0; i < level; i++) { for (var j = 0; j < numPerLine; j++) { //包括的な要素のパラメータ var initOpt = { x : パディング + j * (サイズ + ギャップ)、y: パディング + i * (サイズ + ギャップ)、サイズ: サイズ、速度: 速度、ステータス: 敵タイプ、敵アイコン: opts.enemieIcon、敵ブームアイコン: opts.emoneBoomIcon }; 敵を返す(新しい敵(initOpt)) };5. モンスターのアップデート
モンスター配列のx値を取得し、キャンバスの境界に達しているかどうかを判定します。境界に達している場合、モンスターは下に移動します。同時にモンスターの状態も監視し、通常状態のモンスター、爆発状態のモンスター、消滅したモンスターを配列から削除し、同時に得点する必要がある。
//敵のステータスを更新します updateEnemeis: function () { var opts = this.opts; var neighbors = this.enemies; // 敵の居場所var 敵X = get水平境界(敵); if (敵X.minX < this.敵MinX || 敵X.maxX >= this.敵MaxX) { console.log('enemiesX.minX', 敵X.minX); console.log('敵X.maxX', 敵X.maxX); opts.enemiesDirection = opts.enemiesDirection === '右' : '右'; console.log('opts.emoneDirection', opts.emoneDirection); isFall = true } //敵をループ更新します (i--) { var 敵= 敵[i]; if (isFall) { 敵.down(); } 敵.方向(opts.敵方向); ケース '通常': if (plane.hasHit(敵)) .booming(); } ブレーク; ケース 'ブーミング': 敵.splice(i, 1);デフォルト: ブレーク; } } 、
get水平境界関数の機能は、配列の各要素の x 値を走査し、大きい値または小さい値をフィルターで除外し、配列の最大および最小の x 値を取得することです。
//配列の水平方向の境界関数を取得 gethorizontalBoundary(array) { var min, max; array.forEach(function (item) { if (!min && !max) { min = item.x; max = item.x; } else { if (item.x < min) { min = item.x; } if (item.x > max) { max = item.x } });最大 }}6. キーボードパネルを更新する
Enter キーを押して stop() 関数を実行し、左ボタンを押して航空機を左に移動し、右ボタンを押して航空機を右に動かし、スペース ボタンを押して航空機が弾を発射することを実行します。直線で接続しない場合は、ここで pressedUp と keyBoard.pressedSpace を false に設定します。
updatePanel: function () { var plan = this.plane; var keyBoard = this.keyBoard; if (keyBoard.pressedEnter) { this.stop(); } if (keyBoard.pressedLeft || keyBoard.heldLeft) { 平面。方向('左'); } if (keyBoard.pressedRight || keyBoard.heldRight) { 平面.方向('右') } (keyBoard.pressedUp || keyBoard.pressedSpace) { keyBoard.pressedUp = false; keyBoard.pressedSpace = false() };7.すべての要素を描画します
描画: function () { this.renderScore(); this.plane.draw(); this.enemies.forEach(function (enemies) { //console.log('draw:this.enemies',enemies); 敵。描く(); }); }、8. すべての要素を更新します
まず、モンスター配列の長さが 0 であるかどうかを確認します。それが 0 で、レベルが totalLevel に等しい場合は、レベルが渡されたことを意味します。それ以外の場合は、y 座標が次のレベルのゲーム準備画面が表示されます。モンスター配列の値が航空機の y 座標にモンスターの高さを加えた値より大きい場合は、ゲームが失敗したことを示します。
キャンバス アニメーションの原理は、キャンバスを継続的に描画、更新、クリアすることです。
ゲーム一時停止の原則は、requestAnimationFrame() 関数の実行を防ぐことであり、要素をリセットすることではありません。そのため、ステータスの状態が停止と判断された場合には関数が飛び出します。
//すべての要素のステータスを更新します updateElement: function () { var self = this var opts = this.opts var neighbors = this.enemies; if (enemies.length === 0) { if (opts.level = == opts.totalLevel) { this.end('all-success') } else { this.end('success') } if (enemies[enemies.length]; - 1].y >= this.planePosY - opts.emoneSize) { this.end('failed') } //キャンバスをクリア ctx.clearRect(0, 0, CanvasWidth, CanvasHeight); Canvas this .draw(); //要素のステータスを更新 this.updatePanel(); //継続的にループ updateElement requestAnimationFrame(); if(self.status === 'stop'){ return; }else{ self.updateElement() } };最後に書きます
以上の手順でゲームの基本的な機能が完成します。その他のゲームの開始、終了、スコア計算などの制御については説明を省略します。
最適化できる内容: スペースバーを押したままにすると、弾を連続で発射できるようになります。しかし、再度方向キーを押してみると、弾が発射できなくなってしまった。移動できるときに弾を撃ち続けられるのがベストです。
さらに、このゲームは画面の幅と高さを取得してキャンバスのサイズを決定し、タッチイベント (タッチスタート、タッチムーブ) に変更してゲームをプレイすることができます。 、タッチエンド)、モンスターの見た目も変更でき、画面上部からランダムに降ってくるように変更したり、モンスターの体力が増加したり(4回撃つと消滅するなど)します。
ダウンロードアドレス: https://github.com/littleyljy/shoot
以上がこの記事の全内容です。皆様の学習のお役に立てれば幸いです。また、VeVb Wulin Network をご支援いただければ幸いです。