PixiJSをかじる
Phaserでも使っているというPixiJSの入門を読んでみた。
初心者による超初心者のためのPixiJS入門
VSCodeでコメントを解除していくだけで動作が確認できる。非常に親切な解説だった。
チュートリアル
チュートリアルもひとつだけ読んでみた。01/ Getting started with PixiJSでexamplesのコードがダウンロードできる。その中の17_treasureHunter.htmlを読んでみた。
ここで試せる。PCで矢印キーを使う。コードの概要は以下のようになっていた。Pixijsは衝突判定を自分で書く必要がある。関数も自分で呼び出している。
let loader = PIXI.loader;
let app = new Application({...});
document.body.appendChild(app.view);
loader.add(...).load(setup);
function setup() {
初期化コード(配置やキー処理等)
state = play;
app.ticker.add(delta => gameLoop(delta));
}
function gameLoop(delta) {
state(delta); // playやendを呼び出す
}
function play(delta) {
衝突判定
体力ゲージ更新
宝物を持つ
if (体力が無くなったら) {
state = end;
message.text = 負け
}
if (宝物がドアまで来たら) {
state = end;
message.text = 勝ち
}
}
function end() {
ゲームシーン非表示
ゲームオーバー表示
}
function contail(sprite, container) {
containerにspriteを収める
}
function hitTestRectangle(r1, r2) {
衝突判定
}
function randomInt(min, max) {
整数の乱数生成
}
function keyboard(keyCode) {
キーpress,releaseハンドラ登録
}
メイン処理の所までコメントを日本語にしてみた。
<!doctype html>
<meta charset="utf-8">
<title>Treasure hunter</title>
<body>
<script src="../pixi/pixi.min.js"></script>
<script>
// PIXIを省略するエイリアス
let Application = PIXI.Application,
Container = PIXI.Container,
loader = PIXI.loader,
resources = PIXI.loader.resources,
Graphics = PIXI.Graphics,
TextureCache = PIXI.utils.TextureCache,
Sprite = PIXI.Sprite,
Text = PIXI.Text,
TextStyle = PIXI.TextStyle;
// Pixiアプリケーション生成
let app = new Application({
width: 512,
height: 512,
antialiasing: true,
transparent: false,
resolution: 1
}
);
// HTMLドキュメントに自動的にcanvasを追加する
document.body.appendChild(app.view);
loader
.add("images/treasureHunter.json")
.load(setup);
// 一つ以上の関数で使う可能性のある変数を定義
//
let state, explorer, treasure, blobs, chimes, exit, player, dungeon,
door, healthBar, message, gameScene, gameOverScene, enemies, id;
function setup() { // 初期化関数
// ゲームシーンを作成しステージに追加する
gameScene = new Container();
app.stage.addChild(gameScene);
// スプライトを作ってゲームシーンに追加する
// Create an alias for the texture atlas frame ids
id = resources["images/treasureHunter.json"].textures;
// ダンジョン
dungeon = new Sprite(id["dungeon.png"]);
gameScene.addChild(dungeon);
// ドア
door = new Sprite(id["door.png"]);
door.position.set(32, 0);
gameScene.addChild(door);
// 冒険者
explorer = new Sprite(id["explorer.png"]);
explorer.x = 68;
explorer.y = gameScene.height / 2 - explorer.height / 2;
explorer.vx = 0;
explorer.vy = 0;
gameScene.addChild(explorer);
// 宝物
treasure = new Sprite(id["treasure.png"]);
treasure.x = gameScene.width - treasure.width - 48;
treasure.y = gameScene.height / 2 - treasure.height / 2;
gameScene.addChild(treasure);
// ブロブ(モンスター)を作る
let numberOfBlobs = 6,
spacing = 48,
xOffset = 150,
speed = 2,
direction = 1;
// ブロブモンスターを格納する配列
blobs = [];
// numberOfBlobsの値分のブロブを作る
for (let i = 0; i < numberOfBlobs; i++) {
// ブロブを作る
let blob = new Sprite(id["blob.png"]);
// spacing毎に水平にブロブを配置
// 最初のブロブはスクリーンの左から
// xOffset分空けて配置
let x = spacing * i + xOffset;
// Y軸はランダムでブロブを配置
let y = randomInt(0, app.stage.height - blob.height);
// ブロブの位置を設定
blob.x = x;
blob.y = y;
// ブロブの垂直速度を設定する。方向は1または-1。
// 1は下向き-1は上向き
// ブロブの垂直方向は
// 方向と速度を掛け合わせることで決める。
blob.vy = speed * direction;
// 次のブロブ用に方向を反転させる
direction *= -1;
// ブロブ配列に追加する
blobs.push(blob);
// ゲームシーンにブロブを追加する
gameScene.addChild(blob);
}
// 体力バーを作る
healthBar = new Container();
healthBar.position.set(app.stage.width - 170, 4)
gameScene.addChild(healthBar);
// (体力バーの)黒の背景の長方形を作る
let innerBar = new Graphics();
innerBar.beginFill(0x000000);
innerBar.drawRect(0, 0, 128, 8);
innerBar.endFill();
healthBar.addChild(innerBar);
// (体力バーの)正面の赤い長方形を作る
let outerBar = new Graphics();
outerBar.beginFill(0xFF3300);
outerBar.drawRect(0, 0, 128, 8);
outerBar.endFill();
healthBar.addChild(outerBar);
healthBar.outer = outerBar;
// ゲームオーバーシーンを作る
gameOverScene = new Container();
app.stage.addChild(gameOverScene);
// ゲーム開始時はゲームオーバーシーンは非表示にする
gameOverScene.visible = false;
// テキストスプライトを作りゲームオーバーシーンに追加する
let style = new TextStyle({
fontFamily: "Futura",
fontSize: 64,
fill: "white"
});
message = new Text("The End!", style);
message.x = 120;
message.y = app.stage.height / 2 - 32;
gameOverScene.addChild(message);
// キーボードの矢印キーをキャプチャーする
let left = keyboard(37),
up = keyboard(38),
right = keyboard(39),
down = keyboard(40);
// 左矢印キーを押したときに呼ばれるpressメソッド
left.press = function() {
// キーが押されたとき冒険者の速度を変更する
explorer.vx = -5;
explorer.vy = 0;
};
// 左矢印キーを離したときに呼ばれるreleaseメソッド
left.release = function() {
// 左矢印キー離したとき、右矢印キーを押してなく
// 冒険者が垂直方向に動いてなければ
// 冒険者を停止させる
if (!right.isDown && explorer.vy === 0) {
explorer.vx = 0;
}
};
// 上
up.press = function() {
explorer.vy = -5;
explorer.vx = 0;
};
up.release = function() {
if (!down.isDown && explorer.vx === 0) {
explorer.vy = 0;
}
};
// 右
right.press = function() {
explorer.vx = 5;
explorer.vy = 0;
};
right.release = function() {
if (!left.isDown && explorer.vy === 0) {
explorer.vx = 0;
}
};
// 下
down.press = function() {
explorer.vy = 5;
explorer.vx = 0;
};
down.release = function() {
if (!up.isDown && explorer.vx === 0) {
explorer.vy = 0;
}
};
// ゲームの状態を設定
state = play;
// ゲームループを開始
app.ticker.add(delta => gameLoop(delta));
}
function gameLoop(delta){
// カレントのゲーム状態を更新
state(delta);
}
function play(delta) {
// 冒険者の速度を使って移動する
explorer.x += explorer.vx;
explorer.y += explorer.vy;
// 冒険者をダンジョンエリア内に入れる
contain(explorer, {x: 28, y: 10, width: 488, height: 480});
//contain(explorer, stage);
// 衝突判定前にexplorerHitをfalseにする
let explorerHit = false;
// 敵の配列内のスプライトをループ
blobs.forEach(function(blob) {
//ブロブを移動
blob.y += blob.vy;
// スクリーンの境界チェック
let blobHitsWall = contain(blob, {x: 28, y: 10, width: 488, height: 480});
// ブロブがステージの上や下にぶつかったら
// 方向を反転
if (blobHitsWall === "top" || blobHitsWall === "bottom") {
blob.vy *= -1;
}
// 衝突判定
// 敵と冒険者が触れていたらexplorerHitをtrueに
if(hitTestRectangle(explorer, blob)) {
explorerHit = true;
}
});
// 冒険者がヒットしていたら...
if(explorerHit) {
// 冒険者を半透明
explorer.alpha = 0.5;
// 体力バーの内部の長方形の幅を1pixel減らす
healthBar.outer.width -= 1;
} else {
// ヒットしていなければ冒険者を不透明に
explorer.alpha = 1;
}
// 冒険者と宝物の衝突判定
if (hitTestRectangle(explorer, treasure)) {
// 宝物が冒険者に触れていたら冒険者の近くに
treasure.x = explorer.x + 8;
treasure.y = explorer.y + 8;
}
// 冒険者に体力はあるか? If the width of the `innerBar`
// 体力が無くなればゲームを終了し負けを表示
if (healthBar.outer.width < 0) {
state = end;
message.text = "You lost!";
}
// 冒険者が宝物を持ち出口まで行けば
// ゲームを終了し負けを表示
if (hitTestRectangle(treasure, door)) {
state = end;
message.text = "You won!";
}
}
function end() {
gameScene.visible = false;
gameOverScene.visible = true;
}
/* Helper functions */
function contain(sprite, container) {
let collision = undefined;
//Left
if (sprite.x < container.x) {
sprite.x = container.x;
collision = "left";
}
//Top
if (sprite.y < container.y) {
sprite.y = container.y;
collision = "top";
}
//Right
if (sprite.x + sprite.width > container.width) {
sprite.x = container.width - sprite.width;
collision = "right";
}
//Bottom
if (sprite.y + sprite.height > container.height) {
sprite.y = container.height - sprite.height;
collision = "bottom";
}
//Return the `collision` value
return collision;
}
//The `hitTestRectangle` function
function hitTestRectangle(r1, r2) {
//Define the variables we'll need to calculate
let hit, combinedHalfWidths, combinedHalfHeights, vx, vy;
//hit will determine whether there's a collision
hit = false;
//Find the center points of each sprite
r1.centerX = r1.x + r1.width / 2;
r1.centerY = r1.y + r1.height / 2;
r2.centerX = r2.x + r2.width / 2;
r2.centerY = r2.y + r2.height / 2;
//Find the half-widths and half-heights of each sprite
r1.halfWidth = r1.width / 2;
r1.halfHeight = r1.height / 2;
r2.halfWidth = r2.width / 2;
r2.halfHeight = r2.height / 2;
//Calculate the distance vector between the sprites
vx = r1.centerX - r2.centerX;
vy = r1.centerY - r2.centerY;
//Figure out the combined half-widths and half-heights
combinedHalfWidths = r1.halfWidth + r2.halfWidth;
combinedHalfHeights = r1.halfHeight + r2.halfHeight;
//Check for a collision on the x axis
if (Math.abs(vx) < combinedHalfWidths) {
//A collision might be occurring. Check for a collision on the y axis
if (Math.abs(vy) < combinedHalfHeights) {
//There's definitely a collision happening
hit = true;
} else {
//There's no collision on the y axis
hit = false;
}
} else {
//There's no collision on the x axis
hit = false;
}
//`hit` will be either `true` or `false`
return hit;
};
//The `randomInt` helper function
function randomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
//The `keyboard` helper function
function keyboard(keyCode) {
var key = {};
key.code = keyCode;
key.isDown = false;
key.isUp = true;
key.press = undefined;
key.release = undefined;
//The `downHandler`
key.downHandler = function(event) {
if (event.keyCode === key.code) {
if (key.isUp && key.press) key.press();
key.isDown = true;
key.isUp = false;
}
event.preventDefault();
};
//The `upHandler`
key.upHandler = function(event) {
if (event.keyCode === key.code) {
if (key.isDown && key.release) key.release();
key.isDown = false;
key.isUp = true;
}
event.preventDefault();
};
//Attach event listeners
window.addEventListener(
"keydown", key.downHandler.bind(key), false
);
window.addEventListener(
"keyup", key.upHandler.bind(key), false
);
return key;
}
</script>
</body>