ちびキャラでシューティングゲームの試作
サキュバス娘があらわれた!
オナホガンで撃退しよう!
使用ソフト
| StableDiffusion | ベース絵の出力。 |
| GIMP | ちびキャラの背景切り抜きに使用。 |
| Inkscape | オナホガンから出る弾と、右下のキノコボタンの製作に使用。 |
使用フォント
| ニコモジ | オープンフォントライセンスで配布されているWebフォント。英数ひらがなカタカナだけだけど気に入って愛用している。 |
反省
………今日のおかずを今日のうちに、を標語に手軽なオカズ作りを目指していたハズがどうしてこうなった。
すべてはAIイラストでこんなかわいいチビキャラが出せることを知ってしまったのがイケナイのです。こんなキャラ大量生産できると知ったらゲーム作りたくなるじゃん!
尚、シューティングゲームとか作るの生まれてはじめてなので、いろえろ雑で、敵も1体だけとか手抜きも良いとこですが、とりあえずスマホで動くかテストしたいがためのUPなので何卒お許しを…。
ベース絵
背景
Prompt:
sky background,retro,wallpaper
Negative prompt: (worst quality, low quality:1.2)
Steps: 20, Sampler: Euler a, CFG scale: 7, Seed: 2356490385, Size: 1024x1024, Model hash: fdffd3c312, Model: ambientmix-half, Denoising strength: 0.75, Clip skip: 2
実際はこの左半分だけの1024x1024pxの背景をAIイラストで出力したのだけれど、無限スクロールにするため左右反転して繋げた。
ちびキャラ
Prompt:
1girl,loli,chibi
,shooting with a pink gun
,looking afar,full body,from side,simple white background
Negative prompt: (worst quality, low quality:1.2),(shadow)
Steps: 20, Sampler: Euler a, CFG scale: 7, Seed: 2521445267, Size: 512x512, Model hash: fdffd3c312, Model: ambientmix-half, Denoising strength: 0.75, Clip skip: 2
「オナホールガン」などと指定しても出るわけもないので、shooting with pink gunでひたすらガチャ。
武器とキャラは分けて出力して後で合成したほうが、キャラの武器変更機能とかに対応しやすそうだけど、AIイラストだからサイズや方向もバラバラだし、ガチャがなかなか大変そう。
Prompt:
1girl,loli,chibi
,succubus,flying,pink eyes,smile
,looking far,full body,from side,simple white background
Negative prompt: (worst quality, low quality:1.2)
Steps: 20, Sampler: Euler a, CFG scale: 7, Seed: 2232731296, Size: 512x512, Model hash: fdffd3c312, Model: ambientmix-half, Denoising strength: 0.75, Clip skip: 2, Mask blur: 4
「succubus,flying」指定でコレ系のかわいいチビキャラがばんばん出てくる。幸せ。
AI以外の素材
オナホガンから出る弾。Inkscapeで作成。いらないと思うけど、万が一使いたい人いたらお好きにどうぞ。
最初PCのキーボード操作だけで作っていて、「あ、スマホでどうやって弾撃たせれば良いんだろ?」と気が付き、あわててキノコボタン作成。
技術解説
30年ほど前、ゲーム会社とSIerの会社で迷って給料の高い後者を選んで以来、ゲームの開発からは縁遠い生活だったのだけど、ふとJavaScriptでシューティングゲーム作ってる人とかおるよな?どうやってるんだろ…と調べていて、下記ページを発見。
https://sbfl.net/blog/2016/05/18/javascript-danmaku-stg-1/秒間60フレームのループを作り、毎秒画面全体を描きかえるとか嘘やろ…? 30年前にMS-DOSでV-Sync割り込みで描画していた頃と何も変わってないじゃないか…と驚きと共に何だか懐かしい気分になったので、ノリで作りました。
とてもわかりやすい記事を書いてくれた上記のブログに心から感謝。
JavaScriptでのゲーム作成に興味ある方は上のブログを読むのが一番だけど、一応メインの流れだけ軽く解説しておきます。
class Game {
constructor(ctx) {
this.prev_time = 0;
this.fps = 60;
this.max_width = 1024;
this.max_height = 1024;
this.objects = new Array();
this.ctx = ctx;
this.gameover = false;
}
loop(timestamp) {
if (timestamp - this.prev_time < (1000 / this.fps)) {
requestAnimationFrame(this.loop.bind(this));
return;
}
this.ctx.clearRect(0, 0, this.max_width, this.max_height);
this.objects.forEach((obj) => obj.draw());
if (!this.gameover) {
requestAnimationFrame(this.loop.bind(this));
}
this.prev_time = timestamp;
}
}
今までのミニゲームはCSSアニメーションや、setIntervalと複数のcanvasをレイヤーに見立てた、よく言えば今どきの悪く言えばオモチャのような作りで、今回のコレとは全然別物。
こちらは秒間60回呼び出される処理の中で背景もキャラもHPバーもボタンも毎回全部1つのcanvasに描画しなおすという作りで、移動速度も弾の速度も全てフレーム単位で管理する手法。
ゲーム開発は門外漢なのでよくわからないけれど、たぶんキャラ数や弾数が増えてくるほどこちらの作りのほうがラグが少なくなるのだろう…と思う。
また、プレイヤーキャラも敵キャラも背景やボタンに至るまで、すべてはゲームオブジェクトのひとつという考え方で、すべてがクラスとして実装され、1/60秒ごとにそのクラスのdrawメソッドが呼び出される仕組み。
class GameObject {
constructor(game, imgkey, x, y, hitArea) {
this.game = game;
this.imgkey = imgkey;
this.hitArea = hitArea;
this._hitAreaOffsetX = hitArea.x;
this._hitAreaOffsetY = hitArea.y;
this.x = x;
this.y = y;
}
get x() {
return this._x;
}
set x(value) {
this._x = value;
this.hitArea.x = value + this._hitAreaOffsetX;
}
get y() {
return this._y;
}
set y(value) {
this._y = value;
this.hitArea.y = value + this._hitAreaOffsetY;
}
}
特に感心したのが、全てのGameObjectにはhitAreaというクラスを持たせていて、そのオブジェクトの大きさと弾などがヒットする範囲をきちんと別で管理しているところ。
ゲーム開発においては当たり前なのかも知れないが、畑違いなこともあってなるほどと唸りながら参考にさせてもらった。
尚、ゲームエンジンとして作られているのでそのまま持ってくることも出来たのだろうけど、それでは勉強にならないので、フルスクラッチで1から作っている。
…が、最初はクラスすら使わずに作りはじめたものの、結局継承を使わないとコードが長くなり過ぎる部分があったり、ヒットの判定にはRectangleクラスのほうが楽じゃん、となってどんどん元のソースに近くなった。素人が料理を見様見真似で作ろうとして余計時間がかかり、結局レシピどおりに作るのが一番良かった、という事象に近い気がする。
それから、ポインティングデバイス(&タッチ操作)については一切ゲームエンジンにもなかったため、こちらは後付けで実装した。
let pointerdown = false;
let pointerdown_clientX = 0;
let pointerdown_clientY = 0;
document.getElementById('canvas').addEventListener('pointerdown', function () {
pointerdown = true;
pointerdown_clientX = event.clientX;
pointerdown_clientY = event.clientY;
});
document.getElementById('canvas').addEventListener('pointermove', function () {
if (pointerdown) {
let y_diff = event.clientY - pointerdown_clientY;
let x_diff = event.clientX - pointerdown_clientX;
if (y_diff >= 10) { //↓方向へスワイプ
player.y_add = 6;
} else if (y_diff >= 3) { //↓方向へスワイプ
player.y_add = 3;
} else if (y_diff <= -10) { //↑方向へスワイプ
player.y_add = -10;
} else if (y_diff <= -3) { //↑方向へスワイプ
player.y_add = -3;
} else {
player.y_add = 0;
}
if (x_diff >= 10) { //→方向へスワイプ
player.x_add = 6;
} else if (x_diff >= 3) { //→方向へスワイプ
player.x_add = 3;
} else if (x_diff <= -10) { //←方向へスワイプ
player.x_add = -6;
} else if (x_diff <= -3) { //←方向へスワイプ
player.x_add = -3;
} else {
player.x_add = 0;
}
}
});
document.getElementById('canvas').addEventListener('pointerup', function () {
pointerdown = false;
player.y_add = 0;
player.x_add = 0;
});
もっとうまい方法ありそうな気もするけど、とりあえず、雰囲気(?)で。
画面をタッチしてから離さずに移動した量と方向に応じて、そちらへキャラを動かしている。
画面全体でこの操作が有効なのだけれど、これは画面左下にコントローラーみたいな表示があったほうがわかりやすいのかも知れない。
あるいは、もっと単純に画面をタッチした方向へキャラが動くという作りのほうが楽だった気もする。
また作りながら検討してみる。
まとめ
ミニゲーム作りたーのしー!















