まず初めに
phina.jsだけで完結できるところを、 あえて Nuxt.jsをかぶせて作ってます。
できたクリッカーゲームがこちら↓
鬼姫無双-ONIHIMEMUSO-
最終的に使ったモノ
ゲームライブラリ:phina.js
フレームワーク:Nuxt.js
データ:Firebase「Cloud Firestore」
サーバー:Heroku
CDN:Cloudinary
WebPush:OneSignal
ドロワーソフト:Sketch App
画像編集:Photoshop
作成環境:phina-ts-seed + create-nuxt-app
作った手順
- ゲームデザイン
- まずコンパイルが早いparcelで、phina.jsを使いゲーム自体を作る
- 中だるみを避けるためにここでキャラデザする
- Nuxt.jsを乗せてデータ連携・phinaとのつなぎ込み
- 細かい描写を入れていく
ゲームデザイン
キャラの設定や背景設定など説明すると、工数が大幅にオーバーすると考えたので、まずローンチまで持っていきたかった。
作りながら変わっていきましたが、軸はブレずに作りました。
・横スクロールで
・オリジナルキャラが
・賞金を獲得するために
・敵の群れを倒し
・賞金首を狩りに
・転生するボスを
・一騎打ちで倒すループ
軸を元に最低限必要そうな要素を盛り込んだ形です
・ランキング
・ストーリーモード
・ポーズ
・チュートリアル
・環境設定
phina.jsでゲームを作る
とても素晴らしいゲームライブラリです。
諸先輩方がQiitaにTipsをたくさん残して頂いていて、かつSlackのコミュニティにもTipsが転がっているので、
興味ある方はぜひ是非参加してみてください。
Slack (https://phinajs-slackin.herokuapp.com/)
Typescript
今回型違いの余計なバグを減らしたかったので、ゲーム内のみTypescriptを使い進めていきました。
ゲーム内で使用する変数等が多くなると予想できたので、是非使ってみたかった。
phina-ts-seed を使い出来るだけ早くゲーム部分を作ってしまいたかったので、
ts以下に平たく置きました。
ディレクトリ
ts
┣conf_game.ts - phina.js全体で使用する変数
┣conf_main.ts - ゲーム内で使用する変数
┣declare.ts - phina.js依存の型定義が必要なクラスやinterface。例 declare var Button:any;
┣game.ts - phina.jsのメインロジック
┣scene_loading.ts - ローディングシーン
┣scene_main.ts - メインシーン
┣scene_result.ts - 対戦結果シーン
┣scene_splash.ts - スプラッシュシーン
┗scene_title.ts - タイトルシーン
game.ts
phina.jsのテンプレほぼままで使ってます
import { C } from "./conf_game";
phina.main(function() {
let app = GameApp({
width: C.SCREEN_WIDTH,
height: C.SCREEN_HEIGHT,
assets: C.ASSETS_INIT,
domElement: document.getElementById('phina-canvas'),
fit: false,
fps: 60,
startLabel: 'scene_title',
scenes: [
{
className: 'Scene_Splash',
label: 'scene_splash',
nextLabel: 'scene_title',
},
{
className: 'Scene_Title',
label: 'scene_title',
nextLabel: 'scene_main',
},
{
className: 'Scene_Main',
label: 'scene_main',
nextLabel: 'scene_result',
},
{
className: 'Scene_Result',
label: 'scene_result',
nextLabel: 'scene_title',
},
],
backgroundColor: 'rgba(0, 0, 0, 1)',
});
let appended_canvas = app.canvas.domElement.style;
appended_canvas.width = '100%';
appended_canvas.height = 'auto';
// 実行
app.enableStats();
app.run();
});
キャラデザ
Sketch Appを使ってキャラデザしました。
Illustratorは重いので、結局軽量のSketchが一番使いやすかったです。
phina.jsはスプライト画像を1コマ1コマ動かすのですが、書くのが本当に骨が折れました。。
スプライトスタジオなりソフトを知ったのがかなり後半だったので、
結局すべてSketchでパスを引いてガリガリ書いてました。
Nuxt.jsと結合
ゲームがある程度できたら Nuxt.jsの中に phina.jsを入れていきます。
流れをざっくり言うと、
- phina.jsでシーン進行を送る
- components/game.vue の computed でシーンが進行したら、 dispatch で game(phina.jsのシーン名)をおくる
- シーン名の条件によって page/index.vue で表示する内容を切り分ける
- Nuxt側からシーンを変えたい場合 game.app からシーン切り替えする 例:game.app.run()
をぐるぐる回しているイメージです。
ディレクトリ
Nuxt.js v2.2.0
┣assets - scssを置いてます
┣components - phina-ts-seed の game.ts を game.vue に変えて置いてます
┣constant - バージョンや翻訳テキスト等の定数
┣index.d.ts - Typescriptを書くために置いてます
┣layouts - アプリケーションのレイアウト
┣middleware - アプリケーションのミドルウェア
┣modules - Typescriptを書くためにtypescript.jsを置いてます
┣pages - トップ・ランキング・制作の各ページ
┣plugins - 便利なプラグイン達
┣static - 静的ファイル
┣store - Vuex ストア のファイル群
┗ts - phina-ts-seed の tsディレクトリを置きます
Nuxt.jsでphina.jsのシーン進行をキャッチさせる
ts/conf_game.ts
Nuxtで使いたい変数をエクスポート
export namespace game {
// シーン名
export let scene: string = '';
// OPアニメーション後にapp.run()を走らせたり、Nuxtからシーンを進行させる時に使います
export let app: any = '';
}
Storeを用意する
const game_store = {
namespaced: true,
state: {
game: { scene: null, app: null }
},
mutations: {
// phina.jsのゲームとシーンを代入
musation_set_game(state, params) {
if(params.app) {
state.game.scene = params;
}
if(params.scene) {
state.game.scene = params;
}
},
}
actions: {
// phina.jsのゲームとシーンを代入
commit_set_game({ commit, state }, val) {
commit('musation_set_game', val);
}
}
}
1. phina.jsでシーン進行を送る
ts/scene_各シーン.ts
各シーンのinit時で game にパラメーターを代入
import { game } from './conf_game';
/*
* ローディングシーン
*/
phina.define('Scene_Splash', {
superClass: 'DisplayScene',
init: function(options: any) {
this.superInit(options);
// Vue
game.scene = 'Scene_Splash';
game.app = this;
}
}
2. components/game.vue の computed でシーン進行を監視し、 dispatch で game(phina.jsのシーン名)を送る
components/game.vue
<template>
<canvas id="phina-canvas" :data-scene="`${this.game_scene}`"></canvas>
</template>
<script>
import { game } from '../ts/game/conf_game';
export default {
beforeMount: function() {
const self = this;
phina.main(function() {
const app = GameApp({
// game.ts と同じ内容
});
// 初回phina.jsのゲームとシーンを送る
self.$store.dispatch('game_store/commit_set_game', {scene: null, app: app});
});
},
computed: {
game_scene: function() {
// ここでシーンが進行したらgameをstoreに送る
this.$store.dispatch('game_store/commit_set_game', game);
// 追加で何かしたければココで
switch (game.scene) {
case 'Scene_Splash':
break;
case 'Scene_Title':
break;
case 'Scene_Main':
break;
case 'Scene_Result':
break;
default:
break;
}
return game.scene;
}
}
}
</script>
テンプレートの:data-scene
は、Nuxtで変数をcomputedさせるために仕込んでいます。
3. シーン名の条件によって page/index.vue で表示する内容を切り分ける
page/index.vue
<template>
<template v-if="game_store.game.scene === 'Scene_Title'">
タイトル
<button type="button" @click="btn_game_start">ゲーム開始</button>
</template>
<template v-if="game_store.game.scene === 'Scene_Result'">
ゲーム結果
<button type="button" @click="btn_return_title">タイトルへ戻る</button>
</template>
</template>
<script>
import { mapState } from 'vuex';
export default {
computed: {
...mapState(['game_store'])
},
methods: {
// ゲーム開始
btn_game_start() {
this.$store.getters['game_store/game'].app.onpointstart();
},
// タイトルに戻る
btn_return_title() {
this.$store.getters['game_store/game'].app.onpointstart('scene_title');
},
}
};
</script>
各ページのボタンで phina.js のシーンが進行させます。
同じ要領で phina.js 側で出した結果の得点等の変数を、Nuxt.js 側で Firebase に入れたり出したりしています。
さいごに
個人プロダクトをリリースするのは本当に大変ですよね。
phina.jsのコミュニティを発見して、諸先輩方の作品を見て刺激を受け、この単純なクリッカーゲーを作り始めたのが2018年の4月。
8月ぐらいには出したいなと考えてましたが、ある程度形になったのが12月でした
canvas のデータをどう繋ぎこもうかなとお考えの方がおられましたら、ぜひ試してみてください。
自分の作ったゲームも何処かの誰かの刺激になればと思います。
みなさんも良い phina.js ライフを!!