(内心)
作ったゲームを沢山の人に触ってもらってリアクションが欲しいなぁ~
そうだ、記事投稿して宣伝しよう!
といった下心で書いた記事です。すみませんでした 遊んでください😆
Web Game 処女作🎉
— hikaru🐧#100DaysOfCode! (@hikaru_firecamp) June 17, 2020
シンプルでストレス発散になるゲームが爆誕!
敵さんを吹っ飛ばして 1,000 G 以上を目指してみてね😆https://t.co/h8w574aGGQ
★アドバイス急募★
『こうしたらもっと面白くなる』など、ご意見ご感想を頂けると嬉しいっす(〃´∪`〃)ゞ#今日の積み上げ #100DaysOfCode DAY 21 pic.twitter.com/HJvFj87zTf
ペンギンがサメさんを倒すゲーム
敵さんを吹っ飛ばして 1,000 G 以上を目指してみてね😆
https://games.westa.io/
0⃣ 結論 / 爆速で開発するために実践したこと
- 1週間のカウントダウンタイマーを設置する
- 適当な企画書を作って全体を把握しならが作業する
- 新しいことは1個までに制限して知っているものを使う
- こだわりたい部分を絞り他を捨てる覚悟を持つ
1️⃣ この記事の対象読者
この記事では1週間で Web ゲームを作る為に何を考え何をしたのかを書きました。
以下に当てはまる人達に参考になれば嬉しいです。
- 好きなことへのこだわりが強くいつまでも作品が完成しない人
- いつもダラダラ期限を伸ばしてしまう人
- このゲームがどんなライブラリ使っているか知りたい人
- 奇特な人
2️⃣ なぜ1週間なの?
個人開発の一番の敵っていかにモチベーションを保つかだと思いませんか?
私の場合1ヶ月もたてば他に興味あることが出てきてしまうのでモチベーションが移ってしまいます。
でも、1週間なら全力で頑張れるちょうどよい長さかな~と考えました。
(それと、1週間ならクオリティ低くても言い訳になるかな と)
3⃣ 1週間という短い期間で終わらせるために意識したこと
(1) 1週間のカウントダウンタイマーを設置する
いつもズルズル伸ばしてしまう癖があるので Twitter で公開日を宣言しました。
そして公開日までのカウントダウンタイマーを常に目に入る位置に設置しておきました。
次に出す Web アプリのリリース日を 6/17 12:00 頃に決めました🎉
— hikaru🐧#100DaysOfCode! (@hikaru_firecamp) June 8, 2020
メリハリ付けるために中途半端でもこのタイミングで出します
写真はちょうどいいカウントダウンタイマーが無かったので作ってみました
▪コードはこちらhttps://t.co/HiGjiROP4R
(8,9行目を編集すれば自分用に使えるよ!) pic.twitter.com/HnjXxktDLb
このカウントダウンタイマー、すっごく効果があったように思います。度々目に入るので程よい緊張感と無機質な圧力をくれます。
さらにシレっと周囲の人たちに公開日を宣言できるのでかなりお勧めです。
⚠ 補足 ⚠
さも予定を守ったように言ってますが結局半日ほど遅く公開してしまいました
ほ ん と す み ま せ ん で し た 😗
(2) 適当な企画書を作って全体を把握しならが作業する
企画書などが無い状態でコーディングすると目に付いた場所から手を付けてしまいがちで視野が局所的になりやすく作業の優先度決めが難しくなります。
簡単なものでも全体像を把握できるものがあると、次にどの部分を実装するか考えるときに『最低でもここを実装しないとだから優先度高めで』みたいなスケジュール管理が容易になります。
今回は以下の様な簡易仕様書をあらかじめ書いておりました。
これから作るミニゲームの簡易仕様書!
— hikaru🐧#100DaysOfCode! (@hikaru_firecamp) June 9, 2020
よくあるクリックゲームですが、ちゃんと作りきれるか不安😇
1週間がんばります〜
(あ、奇しくも一人 web1week みたいになってる)
全然関係ないけど最近 iPad でお絵描きの練習始めました!スマブラやったことあればピンとくるはず😎 pic.twitter.com/oVo5xAUeBe
これのおかげで実装段階での仕様変更が減り、一直線にゴールに向かって実装できるので結構大切なものだと思います。
(3) 新しいことは1個までに制限して知っているものを使う
初めて使う道具が多いと勉強することが増えたり些細なことにはまったりして時間を消費しがちなので、使い慣れた開発環境と使った事のあるライブラリを中心に選定しました。
使い慣れたもの
- VS-Code ... 開発環境
- TypeScript ... 開発言語
- UIKit ... UI コンポーネント
以前から実験レベルで遊んでいたもの
- Three.js ... 3D 描画エンジン
- Cannon.js ... 3D 物理エンジン
初めて使うもの
- Audio API ... BGM や効果音の再生
今回は遊べる Web ゲームを短期間で作ることが目的だったので冒険をしない制約を設けましたが、完成度が低くても未知なる技術を沢山学びたいならどんどん新しい技術を使ってみたらよいと思います。
目的次第でやり方を臨機応変にすることが大事なのかなと😎
(4) こだわりたい部分を絞り他を捨てる覚悟を持つ
こだわりが強いと味のあるイイものができるけど、その代わり完成が遅くなる傾向にあると思います。
今回は『ローポリキャラ達が物理演算で予想外の挙動をする』部分だけこだわりましたが、操作 UI の見た目などはブラウザ標準のプログレスバーを使っていたりと大部分はかなり適当です。
個人的にはゲームの中にブラウザの DOM を混ぜることに違和感とアレルギーを感じるのですが『こだわる部分を絞って他は適当』と大胆に割り切っちゃいました。
他にも、ペンギン/サメ/コイン/木 の当たり判定はすべて立方体(正六面体)で手抜きをしましたが、これは予想に反して『サメさんやコインが地面に刺さってる』ように見えたり『木々があらぶってる』感じになったりヘンテコな世界観の演出に一役買ったように思います。
4⃣ おまけ: 実装 Tips
唐突なおまけの実装 Tips です。
(1) Cannon.js / サメさんの Z 位置(奥行)を固定
Cannon.js は 3 次元物理演算なので XYZ 軸分の動きがあるのですが、サメさん(敵)に関しては奥行方向に動かれると攻撃を当てられなくなるので Z 軸を固定しています。
// CannonJs: サメさんの物理演算用の剛体を生成
const sharkBody = new CANNON.Body({ <省略> });
// ThreeJs: アニメーションループを開始
renderer.setAnimationLoop(() => {
// CannonJs: 1フレーム分の物理演算を実行
world.step(<省略>);
// ★★ サメさんのZ位置を固定する ★★
sharkBody.position.set(
sharkBody.position.x,
sharkBody.position.y,
0); // Z軸を0に変更
});
(2) Cannon.js / 摩擦設定・反発設定
初期値だとほとんど滑ることは無く、滑らせた方が面白そうだったので摩擦と反発の設定をいじっています。
モデルごとに摩擦係数や反発係数を設定できれば直感的だったのですが、Cannon.js では『モデル1とモデル2に対しての摩擦・反発を設定』といった具合に設定が必要でした。
//
// 物理演算ワールドを初期化
//
const world = new CANNON.World();
world.gravity.set(0, -9.82, 0); // m/s²
const floorBodyMaterial = new CANNON.Material(`FloorModel`);
const penguinBodyMaterial = new CANNON.Material(`PenguinModel`);
const sharkBodyMaterial = new CANNON.Material(`SharkModel`);
// 摩擦反発設定: 床とペンギン
world.addContactMaterial(new CANNON.ContactMaterial(
floorBodyMaterial,
penguinBodyMaterial,
{
friction: 0.01, // 摩擦設定 (ペンギンが床を滑るように)
restitution: 0.8, // 反発設定
}
));
// 摩擦反発設定: 床とサメ
world.addContactMaterial(new CANNON.ContactMaterial(
floorBodyMaterial,
sharkBodyMaterial,
{
friction: 0.05, // 摩擦設定
restitution: 0.3, // 反発設定
contactEquationStiffness: 1e8,
contactEquationRelaxation: 3,
}
));
// 摩擦反発設定: ペンギンとサメ
world.addContactMaterial(new CANNON.ContactMaterial(
penguinBodyMaterial,
sharkBodyMaterial,
{
friction: 0.01, // 摩擦設定
restitution: 2.0, // 反発設定 (攻撃を受けたサメが吹っ飛びやすいように)
}
));
(3) Three.js / 同じモデルは使いまわして効率化
今回ゲーム内では同じモデルが大量に出現します。その際に毎回モデルをロードしていると実行効率が悪いので、モデル管理クラスが必要になります。
こういった目的の管理クラス系にはシングルトン実装が最適かと思いますが、TypeScript では module
を使うと簡単にシングルトン実装が可能です。
/** モデル名の型定義 */
export type ModelName = `PENGUIN` | `SHARK` | `TREE` | `COIN`;
/**
* ゲーム内のモデルを管理するモジュール
* シングルトン実装
* モデルをあらかじめロードして、ロード済みのモデルを使いまわすことで効率化
*/
export module ModelManager {
/** ロード済みのモデルを保持 */
const modelMap = new Map<ModelName, THREE.Object3D>();
/** 各種モデルをロードする */
export async function load() {
await loadModel(`PENGUIN`, `models/PenguinJumping.glb`);
await loadModel(`SHARK`, `models/Shark.glb`);
await loadModel(`TREE`, `models/Tree.glb`);
await loadModel(`COIN`, `models/Coin.glb`);
}
/** モデルを複製して取得する */
export function getModel(modelName: ModelName) {
return modelMap.get(modelName)!.clone();
}
/** ロード済みモデルを開放する */
export function dispose() {
// ThreeJs: ロードしたモデルをすべて解放
modelMap.forEach((obj3D, key) => {
GameUtils.disposeObject3D(obj3D);
});
}
async function loadModel(modelName: ModelName, path: string) {
// ThreeJs: モデル読み込み
const obj3D = await GameUtils.loadGltfModel(path);
// リストに追加
modelMap.set(modelName, obj3D);
}
}
使い方
// あらかじめすべてのモデルをロードする
await ModelManager.load();
// ペンギンモデルを取得する (内部的にはロード済みのモデルを複製しているので効率的)
const penguin1 = ModelManager.getModel(`PENGUIN`);
const penguin2 = ModelManager.getModel(`PENGUIN`);
const penguin3 = ModelManager.getModel(`PENGUIN`);
最後に
ゲーム開発中に Twitter でいいねやコメントなどでリアクションをくれた方、開発中のゲームを試してヒントをくれた友人方、本当にありがとうございました。
大変励みになりモチベーションになりました、重ねてお礼申し上げます😆
あと、ここまで読んでまだプレイしていない人!!
↓ やってからリアクションをクレクレ厨😗
ペンギンがサメさんを倒すゲーム
敵さんを吹っ飛ばして 1,000 G 以上を目指してみてね😆
https://games.westa.io/
宣 伝 😆
最近書いた記事です。よかったら合わせて見てくれると嬉しいです(〃´∪`〃)ゞ
【239 種無料】Everything というゲームの 3D モデルがオープン化されました🎉モデルの分離方法を紹介