0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

TypeScriptでスプラトゥーン3ブキ射程2択クイズアプリを作成した

Last updated at Posted at 2024-05-20

はじめに

学習を目的としてTypeScriptを使用してシンプルな2択クイズアプリ「スプラトゥーン3 射程クイズゲーム」を開発しました。2つのブキの中から射程が長いと思うブキを選択するゲームです。スマートフォンのブラウザでプレイすることを想定しています。

使った技術やツール

主に以下の技術やツールを使用しました。今回はフレームワークの有用性を理解するために、敢えてフレームワークを使用せずに開発を行いました。また、プレイヤー情報やクイズレコードの保存なども行わないためデータベースも使用していません。

  • TypeScript
  • HTML
  • CSS
  • Vite
  • Vercel

環境構築

  • gitのインストール
  • Githubでリポジトリ作成
    • git cloneできなかったので、以下を行った
      • SSH認証の設定(SSHキーの発行、GithubでSSHキー登録)
      • プロジェクトのディレクトリを、/OneDrive/ドキュメントから/OneDrive配下に変更(仕組み不明)
  • node.jsのインストール
    • Powershellではnodeコマンドが使えるが、VSCodeのGitbashだと使えなかった。環境変数の編集、PC再起動を行うと解決した。
  • npmのインストール
  • TypeScriptのインストール
    • npm install -g typescript
  • Viteのインストール
    • npm create vite@latest
  • tsconfig.jsonを作成
    • Node.jsがtypescriptを認識するために必要な設定ファイル
  • VercelとGithubリポジトリを連携

ゲームの流れ

  1. 「スタート」をクリックするとクイズスタート
  2. 異なる2つのブキの画像がランダムに表示される
  3. 2つのブキのうち、射程が長いと思うブキの画像をクリックする
  4. 正解であれば「○」、不正解であれば「✕」が表示され次の問題へ
  5. 10問目に回答すると結果画面が表示される

主要な機能

クイズデータの取得と管理

クイズのデータはweapon.jsonから読み込まれ、ランダムに2つのブキが選ばれます。

function getRandomWeaponsIds() {
    // weapon.jsonから全てのidを取得
    const ids = weaponsData.map(weapon => weapon.id);
    
    // ids配列からランダムにidを選択
    // Math.floor(Math.random() * ids.length)は、0からidsの最大インデックス(もしlengthが5であれば最大インデックスは4)の値をとる
    const randomId1 = ids[Math.floor(Math.random() * ids.length)];
    let randomId2 = ids[Math.floor(Math.random() * ids.length)];
    // 同じidが選ばれないようにする
    while(randomId1 === randomId2) {
        randomId2 = ids[Math.floor(Math.random() * ids.length)];
    }

    // 選択したidに基づいてブキのデータを取得
    weapon1Id = weaponsData.find(weapon => weapon.id === randomId1)?.id ?? 0;
    weapon2Id = weaponsData.find(weapon => weapon.id === randomId2)?.id ?? 0;
    if (!weapon1Id || !weapon2Id) {
        console.error('エラー:ブキのデータ取得');
    }
    return { weapon1Id, weapon2Id };
}

画面の切り替え

スタート画面、クイズ画面、結果画面はURLを変えずに遷移させたかったので、あらかじめ全ての画面要素をHTMLに記述して、hiddenクラスを使って表示状態を操作しています。

    // スタートボタンを押したとき
    startButton.addEventListener('click', function() {
        // 画面の切り替え
        startScreen.classList.add(STYLE_HIDDEN);
        quizScreen.classList.remove(STYLE_HIDDEN);

回答履歴の保持

回答履歴は、回答時にquestionHistory配列に要素を追加することで管理しています。

// 出題履歴と回答履歴を保持するための配列
// ts(7034)回避のために変数の型を定義する(let questionHistory = [];だと型推論が機能しない)
type QuestionHistory = {
    question: {
        questionNumber: number;
        weapon1Id: number;
        weapon2Id: number;
    },
    result: {
        userAnswer: number,
        correctAnswer: number,
        isAnswerCorrect: boolean;
    }
};
let questionHistory: QuestionHistory[] = [];
// questionHistoryへのデータ追加を行う関数
function addQuestionHistory(questionNumberCount: number, weapon1Id: number, weapon2Id: number, userAnswer: number, correctAnswer: number) {
    questionHistory.push({
        question: {
            questionNumber: questionNumberCount,
            weapon1Id: weapon1Id,
            weapon2Id: weapon2Id,
        },
        result: {
            userAnswer: userAnswer,
            correctAnswer: correctAnswer,
            isAnswerCorrect: userAnswer === correctAnswer
        }
    });
    console.debug(questionHistory);
}

工夫したレイアウト

結果画面

結果は表で表示したかったので、CSSグリッドレイアウトを使用してみました。また、選択したブキがわかるように、ユーザーが選択したブキには背景を、そうでないブキは白黒表示にしています。

        selectedWeapon.classList.add('selected-weapon-background');

        // ユーザが選択したブキの方だけカラー表示とクラス付与を行う
        if (history.question.weapon1Id !== history.result.userAnswer) {
            img1.style.filter = 'grayscale(100%)';
            img1.style.opacity = '0.5';

            weapon2Cell.appendChild(selectedWeapon);
        } else {
            img2.style.filter = 'grayscale(100%)';
            img2.style.opacity = '0.5';

            weapon1Cell.appendChild(selectedWeapon);
        }
/* ぼかした丸を後ろに重ねたい */
.selected-weapon-background {
  z-index: 1;
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 25px;
  height: 25px;
  border-radius: 70% 50% 70% 60% / 40% 80% 60% 70%;
  background-color: #EAFF3D;
}

フォント

スプラトゥーンっぽいデザインにしたかったので、カスタムフォントを適用しています。漢字に対応していないフォント、アルファベットのみのフォントなどが混在しているので、Unicodeでフォントを適用する文字を指定しています。

@font-face {
  font-family: 'CustomFont1';
  src: url('src/assets/fonts/CP_Revenge.ttf') format('truetype');
  unicode-range:
     U+30FC, /* 長音記号 */
    U+4E00-9FAF, /* 漢字の範囲 */
    U+0030-0039; /* 数字 0-9 */
}

@font-face {
  font-family: 'CustomFont2';
  src: url('src/assets/fonts/IGAFONT.ttf') format('truetype');
  unicode-range: 
    U+3041-309F, /* ひらがな */
    U+30A1-30FA; /* カタカナ */
  }

@font-face {
    font-family: 'Paintball_Beta_3';
    src: url('src/assets/fonts/Paintball_Beta_3.ttf') format('truetype');
}

/* font-weight:normalを@font-faceに指定しても変わらなかったが、h1など個別に指定すると適用された */
/* テキストに若干の角度をつけて日本語フォントが崩れるのを防ぐ */
/* .text {
  transform: rotate(0.05deg);
} */

h1, h2, p, div, text{
  font-weight: normal;
  transform: rotate(0.05deg);
}

body {
  font-family: 'CustomFont1', 'CustomFont2', 'Paintball_Beta_3',  sans-serif;
}

感想

  • Vercelを使ったデプロイが簡単だった。Githubにプッシュしただけでビルドしたファイルをアップロードもせずに変更が適用されていたときは驚いた。
  • 初めてTypeScriptを使ったが変数のnullチェックが面倒に感じた。こういうものなのか、何かよい解決策があるのかわからないままに終わった。
if (startButton && toTitleButton &&
	    startScreen && quizScreen && resultScreen &&
	    weapon1Element && weapon2Element && weapon1Img && weapon2Img &&
	    questionNumberElement &&
	    answerModal && answerImage)
  • ディレクトリ構成やコードの分割化などを工夫してコードの可読性を上げたい。実際にデバッグや機能追加する際にストレスに感じた。
  • レイアウト、CSSが難しかった。CSSフレームワークを使うと楽になると聞くので次回は採用してみたい。

参考文献

0
1
0

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
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?