背景
- Vue3を勉強したいと思った
- Composition APIを使ってみたかった
- どうせなら大好きなハロプロメンバーに関連づけたかった
作ったもの
ゲーム画面
GitHub
環境
- Macbook Air
- VSCode
- Vue3
- Vite
- yarn
- SCSS
本題
ViteをつかってVue3インストール
こちらを参考にVite環境、Vue3環境を構築しました。
https://vitejs.dev/guide/#index-html-and-project-root
スネークゲームのベースを導入
こちらのスネークゲームをベースとさせていただきました。
https://qiita.com/miyauchoi/items/f753e5fa0209ab2034dd
↑ はHTMLファイルで完結しているので、
- App.vue
- src/HelloSnake.vue
に分けて簡単にコンポーネント化しました。
ベースとなるApp.vueでHelloSnake.vueをインポートしています。
HelloSnake.vueにはHTML部分とCSS部分を記載し、JS部分は下記のJSファイルに処理ごとに分離しています。
- src/assets/scripts/gatheringMembersAction.js
- src/assets/scripts/goingTimeAction.js
- src/assets/scripts/setupAction.js
Vue3仕様に変更
Vue3の特徴はComposition APIです。
<script>
タグ内、export default{}
文内の書き方が特に変わるので要注意です。
<script>
import { computed, onMounted, ref, reactive } from "vue";
import {
setupAction,
randomizeMemberIndex,
} from "../assets/scripts/setupAction";
import gatheringMembersAction from "../assets/scripts/gatheringMembersAction";
import { goingTimeAction } from "../assets/scripts/goingTimeAction";
import memberImages from "../assets/library/imagePaths";
import nameLists from "../assets/library/nameLists";
export default {
name: "HelloSnake",
setup() {
let isStart = ref(true);
let gridSize = ref(10); // 10 x 10 マス
let memberIndex = ref(0); // メンバーの位置インデックス
// ヘビに関するデータ
const snake = reactive({
headPos: {
x: 1,
y: 3,
}, // 初期位置
bodyIndexes: [0], // 体の位置インデックスたち
direction: "→", // 進行方向
speed: 400, // 1マス進むのにかかる時間[ms]
});
const isFrameout = computed(() => {
const head = snake.headPos;
return (
head.x < 0 ||
gridSize.value <= head.x ||
head.y < 0 ||
gridSize.value <= head.y
);
});
const snakeHeadIndex = computed(() => {
if (isFrameout.value) return null;
return snake.headPos.y * gridSize.value + snake.headPos.x;
});
// メンバーを集めた?
const isGatheringMember = computed(() => {
return snakeHeadIndex.value === memberIndex.value;
});
// 自己衝突してる?
const isSuicided = computed(() => {
return snake.bodyIndexes.includes(snakeHeadIndex.value);
});
// ゲームオーバー?
const isGameover = computed(() => {
return isSuicided.value || isFrameout.value;
});
// 初期化処理
onMounted(() => {
// メンバーの初期位置決定とキーボード入力定義
memberIndex.value = randomizeMemberIndex(gridSize, memberIndex).value;
snake.value = setupAction(snake).value;
});
const startingGame = () => {
// 時間経過によるスネークのアクション
snake.value = goingTimeAction(
isGameover,
isGatheringMember,
snake,
snakeHeadIndex
).value;
isStart.value = false;
};
const { memberIndexUpdated, snakeUpdated, ImageFilePath } =
gatheringMembersAction(gridSize, memberIndex, snake, isGatheringMember);
memberIndex.value = memberIndexUpdated.value;
Object.assign(snake.bodyIndexes, snakeUpdated.bodyIndexes);
Object.assign(snake.speed, snakeUpdated.speed);
const memberIndexImage = computed(() => ({
"--member-index-image": `url(${ImageFilePath.member})`,
}));
const snakeHeadImage = computed(() => ({
"--snake-head-image": `url(${ImageFilePath.head})`,
}));
const setSnakeBodyImage = (gridIndex) => {
if (!snake.bodyIndexes.includes(gridIndex - 1)) return "";
if (ImageFilePath.body.length === 0) return;
// gridIndexの順番ごとに異なる画像をセットする
return {
"background-image": `url(${
ImageFilePath.body[snake.bodyIndexes.indexOf(gridIndex - 1)]
})`,
"background-position": "center center",
"background-repeat": "no-repeat",
"background-size": "cover",
};
};
return {
isStart,
startingGame,
gridSize,
memberIndex,
snake,
isFrameout,
snakeHeadIndex,
isGameover,
isGatheringMember,
memberIndexImage,
snakeHeadImage,
setSnakeBodyImage,
memberImages,
nameLists,
};
},
};
</script>
Composition APIの特徴
- 処理は全部
setup(){}
関数内に書こう -
setup(){}
関数で定義する変数はref()
でリアクティブにしよう - オブジェクト変数は
reactive()
でリアクティブにしよう - リアクティブにした変数の値を参照するときは
変数.value
と書こう -
setup(){}
関数内で定義した変数、関数を<tamplate>
タグ内でも使いたい場合は、returnに記載しよう
スネークをハロプロメンバー集めに変更
スネークゲームのコンセプトとしては、
リーダーのふくちゃん(譜久村聖)とサブリーダーのえりぽん(生田衣梨奈)を先頭にして、次々と表示されるハロプロメンバーを集めていく
というものにしました。
ふくちゃんとえりぽん
※現状、モーニング娘。'21、Juice=Juice、アンジュルムの3グループのメンバーが出るようにしてます。本当はつばきファクトリーとBEYOOOOONDSもいるよ。
スタート画面
スネークゲームを作った目的の1つはハロプロメンバーを知ってもうことなので、メンバー画像と一緒にしっかり名前も出してあげないといけません。
まずはふくちゃんとえりぽんの紹介と共にスタート画面を準備します。
テンプレートはこんな感じで、isStart
がtrueの場合に表示させます。
<template>
~~中略~~
<div v-if="isStart" class="hellosnake__start">
<p style="font-size: 50px; color: coral; font-family: sans-serif">
HELLO-PROJECT SNAKE GAME
</p>
<button class="hellosnake__start__btn" @click="startingGame()">
START
</button>
<p></p>
<div>
<img :src="memberImages[0]" />
<img :src="memberImages[1]" />
<br />
<h1 class="hellosnake__start__text"> {{ nameLists[0] }}</h1>
<h1 class="hellosnake__start__text"> {{ nameLists[1] }}</h1>
</div>
<p style="font-size: 20px">↑↑ このメンバーで始めまるよ ↑↑</p>
</div>
~~中略~~
</template>
isStart
変数はsetup(){}
関数内で定義したものなので、関数の最後でしっかりreturnしてあります。
<script>
~~中略~~
export default {
name: "HelloSnake",
setup() {
~~中略~~
return {
isStart,
~~中略~~
};
},
};
</script>
初期設定とスタート後の処理
初期設定はこちらに書き出しています。
src/assets/scripts/setupAction.js
やっていることは
- メンバーをランダムに表示する処理
- 矢印を押されたらスネークの進行方向を決める処理
です。
ベースにしたものからほとんど変更はありません。
スタート後の時間が進むにつれて実行する処理はこちらです。
src/assets/scripts/goingTimeAction.js
やっていることはこんな感じ。
- 時間が進むごとにスネークを進める
- ゲームオーバーしてたらスネークは進めない
- メンバーを集めた直後はスネークは進めない
こちらもメンバーを集めた直後にスリープを入れた以外は、ベースにしたものからほとんど変更はありません。
メンバーを集めた時
ふくちゃんとえりぽんがメンバーを集めたら次の処理を実行します。
- 集めたメンバーの画像と名前を3秒間表示します
- 集めたメンバーをスネークの後方に追加します
- スネークの移動速度を少し早くします
- 次のメンバーをランダムな位置に表示します
処理は以下ファイルに書き出しました。
src/assets/scripts/gatheringMembersAction.js
import { watch, ref, reactive } from "vue";
import { randomizeMemberIndex } from "../scripts/setupAction";
import { sleep } from "../scripts/goingTimeAction";
import memberImages from "../library/imagePaths";
export default function gatheringMembersAction(
gridSize,
memberIndex,
snake,
isGatheringMember
) {
const memberIndexUpdated = ref(0);
const snakeUpdated = reactive({});
const ImageFilePath = reactive({
member: "src/assets/images/morning/ishida.jpg",
head: "src/assets/images/morning/fukumura.jpg",
body: ["src/assets/images/morning/ikuta.jpg"],
});
const growUpSnake = () => {
snake.bodyIndexes.unshift(snake.bodyIndexes[0]);
return snake;
};
const speedUpSnake = () => {
snake.speed -= 10;
return snake;
};
const defineImages = () => {
// メンバーの出現の順番
ImageFilePath.member = memberImages[snake.bodyIndexes.length + 1];
// snakeの先頭はふくちゃんで固定
ImageFilePath.head = memberImages[0];
// 集めたメンバーをスネークの後方に足していく
ImageFilePath.body.unshift(memberImages[snake.bodyIndexes.length]);
};
watch(isGatheringMember, async (newValue) => {
if (!newValue) return;
await sleep(3);
// snakeの長さを増やす
snakeUpdated.value = growUpSnake().value;
// snakeのスピードを増やす
snakeUpdated.value = speedUpSnake().value;
// snakeの先頭とメンバー画像のファイルパス指定する
defineImages();
// 次のメンバーの表示位置を決める
memberIndexUpdated.value = randomizeMemberIndex(
gridSize,
memberIndex
).value;
await sleep(2);
});
return {
memberIndexUpdated,
snakeUpdated,
ImageFilePath,
};
}
メンバーを集めたことを示す変数isGatheringMember
をwatchして、
trueの場合に処理を実行しています。
この関数はHelloSnake.vueで呼び出しています。
<script>
~~中略~~
const { memberIndexUpdated, snakeUpdated, ImageFilePath } =
gatheringMembersAction(gridSize, memberIndex, snake, isGatheringMember);
memberIndex.value = memberIndexUpdated.value;
Object.assign(snake.bodyIndexes, snakeUpdated.bodyIndexes);
Object.assign(snake.speed, snakeUpdated.speed);
~~中略~~
</script>
AWS S3で公開
以下ページを参考にサクッとS3で公開しています。
https://qiita.com/kiyokiyo_kzsby/items/77bdb81a1ce1852b30ca
ビルドしたindex.htmlとjs, css, 画像ファイルをS3におくだけ。簡単です。
感想
もともと1枚のHTMLだったスネークゲームをVue3で書き直して、ハロプロメンバー仕様に改造しました。
Vue3のComposition APIはルールさえ覚えてしまえば簡単でした。
RefやReactiveの概念、setup関数内で利用するものはimportが必要などついつい忘れがちな部分はありますが、まあそれも慣れかなと。
今回作ったものだとComposition APIの恩恵を受けるには少し足りない気がしますが、
勉強がてら書き方を学べたのでよかったかなと思います。