1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Vue3でハロプロスネークゲームを作った

Posted at

背景

  • 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{}文内の書き方が特に変わるので要注意です。

HelloSnake.vue
<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の場合に表示させます。

HelloSnake.vue
<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してあります。

HelloSnake.vue
<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

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で呼び出しています。

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の恩恵を受けるには少し足りない気がしますが、
勉強がてら書き方を学べたのでよかったかなと思います。

1
0
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?