ハロウィンゲーム🎃👻🦇 kintone
10月31日はハロウィンですね。そこでkintone でハロウィンのゲームをつくってみました。前回と同じくimoniCampのLT用のネタです。安定のイロモノ季節もの路線です。
ハロウィンゲーム🎃👻🦇
上から降ってくるモンスターをよけながら、キャンディ🍬を集めよう!3回モンスターに捕まるとゲームオーバーです。
かぼちゃのアイコンはICONEさん。ありがとうございます。
かぼちゃ🎃はまっすぐ、お化けは👻左右端までいって折り返しながら、こうもり🦇は左右にバタバタとランダムに飛び回りながらプレイヤーに迫ってきます。きゃーーーーー!
アクションゲームを作るうえでいくつかポイントとなる技術があって、前回のインベーダーたたきのときには、
について紹介しました。今回は、
について触れます。
画面の書き換え処理
コンピューターがディスプレイの画面を書き換える処理というのは、ざっくりいうとコンピューターの中に画面用のメモリ(VRAM)が入っていて、そこの値と画面の座標が紐づいてて、それを書き換えて表示しています。
ですので解像度(画面の細かさ)や色の数が増えるほどコンピューターの負担は大きくなり高い性能が要求されます。
じゃあ今ほど性能がよくなかった昔のパソコンやゲーム機でそれをどうやってたかというと、例えば背景が固定でキャラクターのみを動かす場合、キャラクター部分だけを切り取って動かす機能をハード的に解決したりしていました。スプライト機能といいます。
コンピューターが高性能になって3Dゲームが盛んな今日ではレガシーな技術になりますが、昭和レトロなゲームは大体このような技術を駆使してVRAMを直接操作したりドット絵で描いたスプライトのキャラクターを動かしたりしていました。少ないリソースでハードやソフトを駆使してつくるゲームはまさに職人芸といえると思います。ちょっと話がそれました。
配列処理と座標処理
で、今回はゲームに使用するエリアを二次元配列とし、それを画面上の座標にみたてて処理をします。ゲーム画面は全書き換えです。これは背景もキャラクターも絵文字を使うのでそもそも書き換え対象データが少なく、全画面書き換えでも十分な速度が確保できるからです。
実際の二次元配列部分のコードは以下
//画面データ初期化
scrDat = [
['🟦','🟦','🟦','🟦','🟦','🟦','🟦','🟦'],
['🟦','🟦','🟦','🟦','🟦','🟦','🟦','🟦'],
['🟦','🟦','🟦','🟦','🟦','🟦','🟦','🟦'],
['🟦','🟦','🟦','🟦','🟦','🟦','🟦','🟦'],
['🟦','🟦','🟦','🟦','🟦','🟦','🟦','🟦'],
['🟦','🟦','🟦','🟦','🟦','🟦','🟦','🟦'],
['🟦','🟦','🟦','🟦','🟦','🟦','🟦','🟦'],
['🟦','🟦','🟦','🟦','🟦','🟦','🟦','🟦'],
['🟦','🟦','🟦','🟦','🟦','🟦','🟦','🟦'],
['🟦','🟦','🟦','🟦','🟦','🟦','🟦','🟦'],
['🟦','🟦','🟦','🟦','🟦','🟦','🟦','🟦'],
['🟦','🟦','🟦','🟦','🟦','🟦','🟦','🟦'],
['🟦','🟦','🟦','🟦','🟦','🟦','🟦','🟦'],
['🟦','🟦','🟦','🟦','🟦','🟦','🟦','🟦'],
['🟦','🟦','🟦','🟦','🟦','🟦','🟦','🟦'],
['🟦','🟦','🟦','🟦','🟦','🟦','🟦','🟦'],
];
//モンスター1
ym1 = ym1 + 1;
if (ym1 > 15) {
ym1 = 0;
xm1 = Math.floor(Math.random() * 7);
}
if (ym1 >= 0) {
scrDat [ym1][xm1] = '🎃'; //かぼちゃ
}
以下の手順で元データを作成します。
で、実際の配列の座標は以下のような感じになっています。
上記配列データをkintoneのスペースフィールドに転送したらOK!
//画面書き換え
screen = ''; //初期化
for (let y = 0; y < 16; y++){ //y列
scrX = '';
for (let x = 0; x < 8; x++){ //x行
scrX = scrX + scrDat[y][x];
}
screen = screen + scrX + '\n'; //改行
}
イメージとしては、コンピューターのVRAM上に背景を書き込んでスプライトでキャラクターを重ねて画面へ転送してるような感じです(強引)。
当たり判定(再び)
インベーダーたたきの時にもお話しました当たり判定の話。前回の当たり判定はボタンを押したときの絵文字でやってたので、ロジックとしての当たり判定は結局kintoneまかせで私が考えてつくったものではありませんでした。
しかし今回は、ちゃんと自分で考えた当たり判定なので聞いてください!
//当たり判定
if ((ym1 === yp && xm1 === xp) || (ym2 === yp && xm2 === xp) || (ym3 === yp && xm3 === xp)) {
scrDat [yp][xp] = '😱'; //モンスターに捕まった
rec.record['カウント'].value = parseInt(rec.record['カウント'].value,10) -1;
kintone.app.record.set(rec);
}
if (yc === yp && xc === xp) {
scrDat [yp][xp] = '😋'; //キャンディ食べた
rec.record['スコア'].value = parseInt(rec.record['スコア'].value,10) + 1;
kintone.app.record.set(rec);
}
😱がモンスターに捕まった時、😋がキャンディを食べた時の当たり判定の結果です。それぞれ自分と相手(モンスターやキャンディ)の座標が一致したときに「当たり」としての処理をしてるのですね。
一般的なシューティングゲームとかの当たり判定も同じように座標を用いてやっています。ちなみによく当たり判定が厳しいとか甘いとか言いますが、自機が敵に当たった場合の当たり判定は「当たった当たらなかった」で揉めないよう、確実に当たった場合のみ判定するため表示してるキャラクターのスプライトより一回り小さ目のみえない当たり判定用スプライトを重ねて処理します。だいぶ話がそれました。
今後の課題
いくつか課題は残ってて、特に以下二つが大きいです。
・割り込み処理がむずかしい!
自機を動かす割り込みは、JSカスタマイズのボタンを利用しているので割り込み処理自身のロジックは考えなくてもよいのですが、ゲーム画面を見てるとボタン以外の変なとこ押しちゃうし移動分連打しないといけないしイマイチ。キーボードを使った押しっぱなしで左右に動く割り込みをしたかったのですが思いつきませんでした。
・ゲームバランスがむずかしい!
実際今回のゲーム、もっと遅くてよいしモンスターの動きもべつに左右に動かずともよいとは思いましたが、単純に遅くするとキャラクターの動きや操作性にも粗が出てきます。
ちゃんとインターバルを短くしつつゲームの難易度を調整するには敵の動きに適切なウェイトをはさむ処理が必要になってきます。ここのルーチンを作ったら、初心者モードや上級者モードをつくったり徐々に難易度をあげていく設定にしたりできますが正直めんどい。やめました。^^;
あと自分でゲームを開発する際のあるあるなのですが、開発中に何度もそのゲームをやることによって熟練してしまいスゴク難易度の高いものができてしまうというのもありますね。
まあ難易度設定については、どの層をターゲットにしたゲームにするのかというのもあります。お子様イベント向けならもっと簡単にお化けひとつとかでもいいですね。取ったキャンディの数だけお菓子プレゼントとか。
最後に
テキスト文字を使った簡単なゲーム、最近の3Dゲームのような豪華な演出には及びませんがいかがでしょうか。
大型テーマパークで遊ぶのも楽しいですが、かといって公園でやるおにごっこやかくれんぼの楽しさがそれに劣るものではないように単純なゲームもそれはそれで楽しいものです。なにより自分でゲームを作ってみるというのは面白い!
以下にテンプレートとJSソースを貼っておきます。ではでは!
ハッピーハロウィン🎃👻🦇
(() => {
'use strict';
kintone.events.on(['app.record.create.show',
'app.record.edit.show'
],(event) => {
let scrDat; //画面データ 動的配列で初期化
let screen = ''; //画面
let scrX = ''; //x列設定用
const wait = -8; //スタート時のモンスター出現ウエイト
let xm1 = Math.floor(Math.random() * 7); //モンスター1
let ym1 = 0 + wait;
let xm2 = Math.floor(Math.random() * 7); //モンスター2
let ym2 = -4 + wait;
let mLR = 1;
let xm3 = Math.floor(Math.random() * 7); //モンスター3
let ym3 = -8 + wait;
let xc = Math.floor(Math.random() * 7); //キャンディ
let yc = -12 + wait;
let xp = 3; //プレイヤー
let yp = 15;
//ボタン用スペース作成
const spcB = kintone.app.record.getSpaceElement('spcB');
//ボタン作成
const btnL = document.createElement('button');
const btnR = document.createElement('button');
//ボタン表示初期化
btnL.textContent = '👈👈👈';
btnR.textContent = '👉👉👉';
//スペースにボタン追加
spcB.appendChild(btnL);
spcB.appendChild(btnR);
const spcG = kintone.app.record.getSpaceElement('spcG');
let initFlag = 1; //初期化フラグ
let sc = 0; //スコア
let hisc = 0; //ハイスコア
const timer = window.setInterval(() => {
const rec = kintone.app.record.get();
rec.record['カウント'].value = parseInt(rec.record['カウント'].value,10);
kintone.app.record.set(rec);
//ゲーム終了処理
if (rec.record['カウント'].value <= 0) {
//スコア・ハイスコア取得
sc = parseInt(rec.record['スコア'].value, 10);
hisc = parseInt(rec.record['ハイスコア'].value, 10);
//ゲーム終了メッセージ処理
if (sc > hisc) {
rec.record['ハイスコア'].value = rec.record['スコア'].value;
window.alert('おめでとう!ハイスコア ' + rec.record['ハイスコア'].value);
} else {
window.alert('ゲームオーバー ' + rec.record['スコア'].value);
}
initFlag = 1; //初期化フラグセット
}
//初期化
if (initFlag !== 0) {
initFlag = 0; //初期化フラグリセット
rec.record['スコア'].value = 0;
rec.record['カウント'].value = 3;
kintone.app.record.set(rec);
xm1 = Math.floor(Math.random() * 7); //モンスター1
ym1 = 0 + wait;
xm2 = Math.floor(Math.random() * 7); //モンスター2
ym2 = -4 + wait;
mLR = 1;
xm3 = Math.floor(Math.random() * 7); //モンスター3
ym3 = -8 + wait;
xc = 2; //キャンディ
yc = -12 + wait;
xp = 3; //プレイヤー
yp = 15;
}
//画面データ初期化
scrDat = [
['🟦','🟦','🟦','🟦','🟦','🟦','🟦','🟦'],
['🟦','🟦','🟦','🟦','🟦','🟦','🟦','🟦'],
['🟦','🟦','🟦','🟦','🟦','🟦','🟦','🟦'],
['🟦','🟦','🟦','🟦','🟦','🟦','🟦','🟦'],
['🟦','🟦','🟦','🟦','🟦','🟦','🟦','🟦'],
['🟦','🟦','🟦','🟦','🟦','🟦','🟦','🟦'],
['🟦','🟦','🟦','🟦','🟦','🟦','🟦','🟦'],
['🟦','🟦','🟦','🟦','🟦','🟦','🟦','🟦'],
['🟦','🟦','🟦','🟦','🟦','🟦','🟦','🟦'],
['🟦','🟦','🟦','🟦','🟦','🟦','🟦','🟦'],
['🟦','🟦','🟦','🟦','🟦','🟦','🟦','🟦'],
['🟦','🟦','🟦','🟦','🟦','🟦','🟦','🟦'],
['🟦','🟦','🟦','🟦','🟦','🟦','🟦','🟦'],
['🟦','🟦','🟦','🟦','🟦','🟦','🟦','🟦'],
['🟦','🟦','🟦','🟦','🟦','🟦','🟦','🟦'],
['🟦','🟦','🟦','🟦','🟦','🟦','🟦','🟦'],
];
//モンスター1
ym1 = ym1 + 1;
if (ym1 > 15) {
ym1 = 0;
xm1 = Math.floor(Math.random() * 7);
}
if (ym1 >= 0) {
scrDat [ym1][xm1] = '🎃'; //かぼちゃ
}
//モンスター2
ym2 = ym2 + 1;
if (ym2 > 15) {
ym2 = 0;
xm2 = Math.floor(Math.random() * 7);
}
if (xm2 >= 7) { //左反転
mLR = -1;
}
if (xm2 <= 0) { //右反転
mLR = 1;
}
xm2 = xm2 + mLR;
if (ym2 >= 0) {
scrDat [ym2][xm2] = '👻'; //お化け
}
//モンスター3
ym3 = ym3 + 1;
if (ym3 > 15) {
ym3 = 0;
xm3 = Math.floor(Math.random() * 7);
}
xm3 = xm3 + Math.floor(Math.random() * 3) -1; //左右ブレ乱数
if (xm3 < 0) { //左からはみ出さない
xm3 = 0;
}
if (xm3 > 7) { //右からはみ出さない
xm3 = 7;
}
if (ym3 >= 0) {
scrDat [ym3][xm3] = "🦇"; //こうもり
}
//お菓子
yc = yc + 1;
if (yc > 15) {
yc = 0;
xc = Math.floor(Math.random() * 7);
}
if (yc >= 0) {
scrDat [yc][xc] = '🍬'; //キャンディ
}
//自機
scrDat [yp][xp] = '😀'; //プレイヤー
//当たり判定
if ((ym1 === yp && xm1 === xp) || (ym2 === yp && xm2 === xp) || (ym3 === yp && xm3 === xp)) {
scrDat [yp][xp] = '😱'; //モンスターに捕まった
rec.record['カウント'].value = parseInt(rec.record['カウント'].value,10) -1;
kintone.app.record.set(rec);
}
if (yc === yp && xc === xp) {
scrDat [yp][xp] = '😋'; //キャンディ食べた
rec.record['スコア'].value = parseInt(rec.record['スコア'].value,10) + 1;
kintone.app.record.set(rec);
}
//画面書き換え
screen = ''; //初期化
for (let y = 0; y < 16; y++) { //y列
scrX = '';
for (let x = 0; x < 8; x++) { //x行
scrX = scrX + scrDat[y][x];
}
screen = screen + scrX + '\n'; //改行
}
spcG.innerText = screen;
},167);
//左ボタンクリック
btnL.onclick = () => {
xp = xp - 1;
if (xp < 0) { //左からはみ出さない
xp = 0;
}
};
//右ボタンクリック
btnR.onclick = () => {
xp = xp + 1;
if (xp > 7) { //右からはみ出さない
xp = 7;
}
};
return event;
});
})();