Phina.jsでゲーム作りの基礎

Javascriptのゲームツール

Javascriptでゲームを作るには、色々ツールがあるようです。私も以前はenchant.jsを授業に組み入れて使っていましたが、今回はPhina.jsを使ってみましょう

Phina.jsを使ってみる

Phina.jsは国産のオープンソースで大学や専門学校で利用されているようです。以降このような項目で展開していきます。

  1. Phina.jsのインストール
  2. stepを登りながら実験

Phina.jsのインストール

最初に初期化だけを試したいので、適当なフォルダー配下に step0というフォルダーを作り、その中にindex.htmlとmain.jsを作っていきましょう。phina.jsを使うには、htmlのheadタグの中でphina.jsを読み込んであげるだけでよいのですが、phina.jsのホームページに公開されているCDNアドレスは最近、推奨でなくなったのでこちらにかえてください。

step0\index.html
	<!doctype html>
	<html>
		<head>
		<meta charset='utf-8' />
		<meta name="viewport" content="width=device-width, user-scalable=no" />
		<meta name="apple-mobile-web-app-capable" content="yes" />
		<title>step0 phina.js initial screen</title>
		<!-- phina.js を読み込む -->
		<script src="https://cdn.jsdelivr.net/npm/phina.js@0.2.3/build/phina.min.js"></script>
		</head>
		<body>
		<!-- メイン処理 -->
		<script src='main.js'></script>
		</body>
	</html>

2023/9/15現在の最新は0.2.3なのでこのようになっていますが、最新バージョンが変わったらバージョンを更新してください。

またサイト上の開発環境 runstantがあるので、こちらにloginすれば先輩たちのロジックをみたり、実行したりできるので勉強になります。

では続いてJavascriptをmain.jsとしてstep0フォルダーに追加します。

step0\main.js
	// phina.js をグローバル領域に展開
	phina.globalize();
	
	// MainScene クラスを定義
	phina.define('MainScene', {
		superClass: 'DisplayScene',
		init: function() {
			this.superInit();
			// 背景色を指定
			this.backgroundColor = '#222';
		},
	});
	
	// メイン処理
	phina.main(function() {
		// アプリケーション生成
		var app = GameApp({
		startLabel: 'main', // メインシーンから開始する
		});
		// アプリケーション実行
		app.run();
	});

index.htmlをダブルクリックして実行すると、ただ黒い縦長の領域がブラウザに表示されます。エラーが出ていなければこれでOKです。初期化成功です。

ゲーム用のツールは何も使っても、仕組みはだいたい同じで、

  1. init関数で必要な画面の初期値を宣言する
  2. 登場する図とかキャラクターをクラスとして宣言する
  3. update関数が毎秒15回くらい呼び出されるのでそこで移動の操作をする
  4. 最後にmainがあって、そこで起動をかけている

このstep0ではupdateやクラス宣言はありませんが、次からでてきます。

phina.js initial

stepを登りながら実験

では、続いてstep1というフォルダーを作りましょう。内容はstep0のコピーで構いません。この後の各stepでもstep1,step2と作って行きましょう。

step1 シェイプを真ん中に表示

index.htmlはほとんど変わらないので省略します

step1\main.js
	// phina.js をグローバル領域に展開
	phina.globalize();
	
	// MainScene クラスを定義
	phina.define('MainScene', {
		superClass: 'DisplayScene',
		init: function() {
			this.superInit();
			// 背景色を指定
			this.backgroundColor = '#aaa';
		
			// 丸のシェイプを表示
			let circle = CircleShape().addChildTo(this);
			circle.x = this.width /2 ;
			circle.y = this.height / 2;
			circle.fill = 'green';
			circle.stroke = 'black'
			circle.strokeWidth= 8;
			circle.radius = 40;
		},
	});
	
	// メイン処理
	phina.main(function() {
		// アプリケーション生成
		var app = GameApp({
		startLabel: 'main', // メインシーンから開始する
		});
		// アプリケーション実行
		app.run();
	});

丸い図形シェイプを画面の真ん中に表示するだけです。

phina.js step1

step0からすると、少し変わっています。まずinit内でCircleShapeというクラスからオブジェクトをつくっています。これは単純な図形を出すもので、その下でその場所や大きさを宣言しています。

step2 シェイプを左右に移動

index.htmlはほとんど変わらないので省略します。

step2\main.js
	// phina.js をグローバル領域に展開
	phina.globalize();

	//画面幅
	let SCREEN_X = 400;
	let SCREEN_Y = 800;

	// MainScene クラスを定義
	phina.define('MainScene', {
		superClass: 'DisplayScene',
		init: function(options) {
			this.superInit(options);
			// 背景色を指定
			this.backgroundColor = '#aaa';

			// 丸のシェイプを表示
			let circle = CircleShape().addChildTo(this);
			circle.x = this.width /2 ;
			circle.y = this.height / 2;
			circle.fill = 'green';
			circle.stroke = 'black'
			circle.strokeWidth= 1;
			circle.radius = 40;

			//更新処理
			let moveto = 5;
			circle.update = function() {
				circle.x += moveto;
				if (circle.x >= (SCREEN_X-(circle.width/2))) {
					moveto *= -1;
				} else if (circle.x <= (0+(circle.width/2)) ) {
					moveto *= -1;
				}
			}
		},
	});

	// メイン処理
	phina.main(function() {
		// アプリケーション生成
		var app = GameApp({
			startLabel: 'main', // メインシーンから開始する
			//screenサイズを変更
			width: SCREEN_X,
			height: SCREEN_Y,
		});
		// アプリケーション実行
		app.run();
	});

丸い図形シェイプを画面の上下真ん中に表示して、左右にバウンドさせます。

phina.js step2

step1からすると、少し変わっています。circleのupdateイベントで、circleのxの位置を変動させます。これが毎秒15回ほど繰り返されます。右の壁に当たったら跳ね返って右に移動を始めます。左の壁に当たったら跳ね返って左に移動します。

step3 スプライトを左右に移動

index.htmlはほとんど変わらないので省略します。

step3\main.js
	// phina.js をグローバル領域に展開
	phina.globalize();

	//画面幅
	let SCREEN_X = 400;
	let SCREEN_Y = 800;

	//アセット
	var ASSETS = {
		//tomapiko トマトのピコ
		image: {
			'tomapiko': 'https://cdn.jsdelivr.net/gh/phinajs/phina.js@develop/assets/images/tomapiko.png',
		},
	};

	// MainScene クラスを定義
	phina.define('MainScene', {
		superClass: 'DisplayScene',
		init: function(options) {
			this.superInit(options);
			// 背景色を指定
			this.backgroundColor = '#aaa';

			//スプライト表示
			var sprite = Sprite('tomapiko').addChildTo(this).setPosition(SCREEN_X/2,SCREEN_Y/2);

			//更新処理
			let moveto = 5;
			sprite.update = function() {
				sprite.x += moveto;
				if (sprite.x >= (SCREEN_X-(sprite.width/2))) {
					moveto *= -1;
				} else if (sprite.x <= (0+(sprite.width/2)) ) {
					moveto *= -1;
				}
			};
		},
	});

	// メイン処理
	phina.main(function() {
		// アプリケーション生成
		var app = GameApp({
			startLabel: 'main', // メインシーンから開始する
			//screenサイズを変更
			width: SCREEN_X,
			height: SCREEN_Y,
			//アセット読み込み
			assets: ASSETS,
		});
		// アプリケーション実行
		app.run();
	});

phinaのcdnからトマピコというキャラクター画像を読み込んで画面の上下真ん中に表示して、左右にバウンドさせます。

phina.js step3

step2との変更点はシェープからスプライトに変わったことです。アニメーションなどで形の変わるものはスプライトを使用します。今回はただ出しているだけなのでちょっと変な感じですね。

step4 鳥をパタパタさせる

index.htmlはほとんど変わらないので省略します。

step4\main.js
	// phina.js をグローバル領域に展開
	phina.globalize();

	//画面幅
	let SCREEN_X = 400;
	let SCREEN_Y = 800;

	//アセット
	var ASSETS = {
		//mikapiyo ミカンのピヨ
		image: {
			'tomapiko': 'https://cdn.jsdelivr.net/gh/phinajs/phina.js@develop/assets/images/tomapiko_ss.png',
		},
		//スプライトシート
		spritesheet: {
			"tomapiko_ss": {
				//フレーム情報
				"frame": {
					"width": 64,
					"height": 64,
					"cols": 6,
					"rows": 3,
				},
				//アニメーション情報
				"animations": {
					"walk": {
						"frames": [12,13,14],
						"next": "walk",
						"frequency": 6,
					},
				}
			},
		}
	};

	// MainScene クラスを定義
	phina.define('MainScene', {
		superClass: 'DisplayScene',
		init: function(options) {
			this.superInit(options);
			// 背景色を指定
			this.backgroundColor = '#aaa';

			//スプライト表示
			var sprite = Sprite('tomapiko').addChildTo(this).setPosition(SCREEN_X/2,SCREEN_Y/2);

			//スプライトにフレームアニメーションをアタッチ
			var anim = FrameAnimation('tomapiko_ss').attachTo(sprite);
			//アニメーションを指定する
			anim.gotoAndPlay('walk');
		},
	});

	// メイン処理
	phina.main(function() {
		// アプリケーション生成
		var app = GameApp({
			startLabel: 'main', // メインシーンから開始する
			//screenサイズを変更
			width: SCREEN_X,
			height: SCREEN_Y,
			//アセット読み込み
			assets: ASSETS,
		});
		// アプリケーション実行
		app.run();
	});

phinaのcdnからトマピコのアニメーション用の画像を読み込んで画面の上下真ん中に表示して、パタパタさせます。なぜアニメーションができるかというと、今回はフレームアニメーションに読み込んだ画像を渡しているからで、その画像はこのようなものです。

phina.js frame animetion

この画像は左上から右下に向かって画像が詰められた状態になっていて、今回は6✕3の行列になっています。左上が0その右が1で並んでいます。12,13,14の3つの素材を連続して切り替えると歩いているように見えるパタパタアニメです。一つ一つの素材の大きさは64✕64ビットで6行3列で有ることも宣言しておきます。

step5 パタパタさせた鳥をキーボードで左右に移動させる

index.htmlはほとんど変わらないので省略します。

step5\main.js
	// phina.js をグローバル領域に展開
	phina.globalize();

	//画面幅
	let SCREEN_X = 400;
	let SCREEN_Y = 800;
	let SPEED = 2;

	//アセット
	var ASSETS = {
		//mikapiyo ミカンのピヨ
		image: {
			'tomapiko': 'https://cdn.jsdelivr.net/gh/phinajs/phina.js@develop/assets/images/tomapiko_ss.png',
		},
		//スプライトシート
		spritesheet: {
			"tomapiko_ss": {
				//フレーム情報
				"frame": {
					"width": 64,
					"height": 64,
					"cols": 6,
					"rows": 3,
				},
				//アニメーション情報
				"animations": {
					"walk": {
						"frames": [12,13,14],
						"next": "walk",
						"frequency": 6,
					},
				}
			},
		}
	};

	// MainScene クラスを定義
	phina.define('MainScene', {
		superClass: 'DisplayScene',
		init: function(options) {
			this.superInit(options);
			// 背景色を指定
			this.backgroundColor = '#aaa';

			//スプライト表示
			var sprite = Sprite('tomapiko').addChildTo(this).setPosition(SCREEN_X/2,SCREEN_Y/2);

			//毎フレーム処理
			this.update = function(app) {
				var key = app.keyboard;
				//左右移動
				if (key.getKey('left')) {
					sprite.x -= SPEED;
					sprite.scaleX = 1;
				}
				if (key.getKey('right')) {
					sprite.x += SPEED;
					sprite.scaleX = -1;
				}
			}

			//スプライトにフレームアニメーションをアタッチ
			var anim = FrameAnimation('tomapiko_ss').attachTo(sprite);
			//アニメーションを指定する
			anim.gotoAndPlay('walk');
		},
	});

	// メイン処理
	phina.main(function() {
		// アプリケーション生成
		var app = GameApp({
			startLabel: 'main', // メインシーンから開始する
			//screenサイズを変更
			width: SCREEN_X,
			height: SCREEN_Y,
			//アセット読み込み
			assets: ASSETS,
		});
		// アプリケーション実行
		app.run();
	});

step4でパタパタした鳥を、キーボードの左右キーで移動させます。同時に向きも変えるようにします。これにはsprite.scaleXのプラスマイナスを切り替えます。

step6 パタパタさせた鳥を背景の前で移動させる

index.htmlはほとんど変わらないので省略します。

step6\main.js
	// phina.js をグローバル領域に展開
	phina.globalize();

	//画面幅
	let SCREEN_X = 400;
	let SCREEN_Y = 800;
	let SPEED = 2;

	//アセット
	var ASSETS = {
		//mikapiyo ミカンのピヨ
		image: {
			'tomapiko': 'https://cdn.jsdelivr.net/gh/phinajs/phina.js@develop/assets/images/tomapiko_ss.png',
			'bg': 'bg.png'
		},
		//スプライトシート
		spritesheet: {
			"tomapiko_ss": {
				//フレーム情報
				"frame": {
					"width": 64,
					"height": 64,
					"cols": 6,
					"rows": 3,
				},
				//アニメーション情報
				"animations": {
					"walk": {
						"frames": [12,13,14],
						"next": "walk",
						"frequency": 6,
					},
				}
			},
		}
	};

	// MainScene クラスを定義
	phina.define('MainScene', {
		superClass: 'DisplayScene',
		init: function(options) {
			this.superInit(options);
			// 背景指定
			this.bg = Sprite("bg").addChildTo(this);
			this.bg.origin.set(0,0);
			this.bg.width = SCREEN_X * 5;
			this.bg.height = SCREEN_Y;

			//スプライト表示
			var sprite = Sprite('tomapiko').addChildTo(this).setPosition(SCREEN_X/2,SCREEN_Y*4/5);

			//毎フレーム処理
			this.update = function(app) {
				var key = app.keyboard;
				//左右移動
				if (key.getKey('left')) {
					if (this.bg.x < 0) 
					this.bg.x += SPEED;
					sprite.scaleX = 1;
				}
				if (key.getKey('right')) {
					if (this.bg.x > -(SCREEN_X*4)) 
					this.bg.x -= SPEED;
					sprite.scaleX = -1;
				}
			}

			//スプライトにフレームアニメーションをアタッチ
			var anim = FrameAnimation('tomapiko_ss').attachTo(sprite);
			//アニメーションを指定する
			anim.gotoAndPlay('walk');
		},
	});

	// メイン処理
	phina.main(function() {
		// アプリケーション生成
		var app = GameApp({
			startLabel: 'main', // メインシーンから開始する
			//screenサイズを変更
			width: SCREEN_X,
			height: SCREEN_Y,
			//アセット読み込み
			assets: ASSETS,
		});
		// アプリケーション実行
		app.run();
	});

step5で動かした鳥を背景の前で動かしましょう。背景は自作でも構いません。私もペイントで適当に作りました。

phina.js 背景

サイズは2000✕800です。一番左端を最初に表示していますので、そこから左には行けません。右の矢印を押して右に移動できます。一度右に移動すれば行った分だけ左に戻れます。

step5では、キーボードで鳥のスプライトを移動していましたが、ここでは逆に鳥を止めたままで、背景を逆に動かします。this.bgで背景を作り、そのthisのchildとして鳥を追加しているので背景の上に鳥が乗っています。

phina.js 背景

step7 自分で改造してみましょう

ここに公開するのはここまでにします。以降は自分なりに改造してみましょう。背景は3分で作れるようなものもあれば、2,3日かかるようなものもあるでしょう。またアニメーション部分はphina.jsにもまだあります。

phina.js のCDNに行くと、そこに利用可能なものがあります。とくにassetsの中に画像がいくつか入っています。今回もここのものを使いました。

また、資料としてはZenn phina.js Tips集 上巻Zenn phina.js Tips集 下巻は必読です。またQiitaのPhina.JSでゲームを作ってみたでは、ダンジョンを作るサンプルとかがあります。背景をブロックのように組み立てるもので面白そうです。

なにか作品を作ろうとすると結局素材がなければなにもできないで終わってしまいます。ユニティチャンは、プログラムはできるけど絵は描けないという人向けにDATAのダウンロードができるようになっています。規約を守っていれば自由に使えるのでやってみる価値はありそうです。データの展開でextractunitypackage.pyが必要になりますので調べて挑戦してみてください。