このサイトは、只今WEB業界で活躍中のデザイナー、プログラマーの方々の情報を集めたweb統合情報サイトです。

Archives Details

2022 VIVA JS World Cup 開幕!! 〜 Vue3で作るサッカーゲーム 〜

JavaScript

2022.12.24

この記事は最終更新日から1年以上が経過しています。


本記事はCocone Advent Calendar 2022の24日目の記事となります。

こんにちは

メリークリスマス!!

いやぁ、しかし先週の日曜日は、 M1グランプリやワールドカップやらで何かと忙しかったですね。

しかし、色々感動しましたね。特にワールドカップの決勝はすんごい試合でしたね。

歴史的な試合ですね。あれは。

日本戦も凄かったですねぁ。前評判は良くなかった日本ですが、いざ本番になるといくつもの奇跡と、いくつも感動と勇気を与えて頂いたと思います。とにかく、あの頑張り姿にとにかく感動しましたね。いやぁ、良かった。

何本もシュートは打っていたのですが、やっぱそう簡単には点が入らないのがワールドカップ。

でも、皆さん思いませんでした?

「今のシュート、キーパーがいなかったら点入っていたよなぁ。」

と。

そう、キーパーがいなかったら何点になっていたか分かりません。

そこで開幕します!!!

2022 VIVA JS World Cup 開幕!!


↓↓↓ こちらのページよりプレイできます。 ↓↓↓

Let’s PLAY ▶▶▶ 2022 VIVA JS World Cup

↑↑↑ こちらのページよりプレイできます。 ↑↑↑

 
GitHub

ソースはこちら
https://github.com/webcyou-org/2022-VIVA-JS-WorldCup

 

遊び方

さぁ、キーパーのいないフィールドでシュートの練習し放題だ!

次回、2026年のワールドカップ目指してひたすら練習だ!

キーボードの十字カーソルで選手を移動。 xキーでシュートパワーを貯め思いっきりシュート!

↑ : ArrowUp or KeyW
→ : ArrowRight or KeyD
↓ : ArrowDown or KeyS
← : ArrowLeft or KeyA
shoot : KeyX

作り方

今回はゲームエンジンなど使用せず、言語はTypeScript、フレームワークとしてVue3を使用しております。

  • vue 3.2.45
  • typescript 4.7.4

ゲーム作成に慣れている方は、真新しい情報は乏しいかもですが、フロントエンド技術を用いて新規でゲーム作成したいと思っている方は参考になる内容かもしれなので、よろしければご参考にしてください。

ゲームループ

何はともあれ、ゲームを作成するにはゲームループが必要である。
お金の使い方として、消費、投資、浪費があるように、ゲームループも3つの基本の要素があります。

  • 「Input」
  • 「Update」
  • 「draw」

この三原則である。
いつの日だってこれが基本である。

const gameInterval = setInterval(gameLoop, 1000 / fps);
const gameLoop = () => {
    // input();
    update();
    draw();
};
プレイヤー

選手達は、Player classとして作成します。主なProps(interface)は以下の通りとなります。

interface PlayerProps {
    x: number;
    y: number;
    width: number;
    height: number;
    speed: number;
}

x, y座標と共に、width、heightと、キャラクター自身のサイズも保持します。その他に、走るスピードも初期の段階で設定可能としておきます。

ドット絵作成

キャラクターのビジュアルはドット絵(ピクセルアート)で作成しました。
何より、こちらの作成が楽しかったのですが、時間掛かりまくった割には、最終的な仕上りの出来はひどいものとなってしまいました(笑)
つくづく、作成の練習の時間が欲しいと感じる今日この頃です。

ドット絵(ピクセルアート)を作成したアプリは、以下の通りとなります。

 

Pixel Studio for pixel art

こちら、無料且つ、iPad対応アプリなので Apple Pencilで書き書きが可能で、ドット絵(ピクセルアート)をサクッと作成するにはもってこいのアプリだと思います。
こちらのアプリでドット絵のベースを作成し、アニメーションなど仕上げの為、aseprite を利用します。

aseprite


こちらもドット絵作成アプリとして有名なアプリとなっており、痒いところまで手が届く色んな事が行えるアプリとなっております。
Windows、Mac、Ubuntuと様々なOSにも対応しております。

残念ながら無料ではなく、 $19.99 USD とお金がかかってしまいますが、無料で使えるtrialバージョンもありますので、まずは無料でtrialを使用し、気に入ったら購入しても良いかもですね。

ドット絵作成のバイブル的な書籍

その他に、ドット絵作成のバイブル的な書籍として、「Pixel Logic」という書籍があります。

Pixel Logic

こちら、日本語版はなく英語版のみとなってしまいますが、そこまで英語の文章も多くなく、絵が多く内容も文字を読まなくても理解しやすい内容となっております。ドット絵に関してはこの一冊でほぼ網羅されている感じはあります。 値段も9$ USDと購入しやすい価格帯 なので気になった方は購入してはいかがでしょうか。
パラパラと見ているだけでも楽しい内容となっております。

プレイヤーのステート

それでは、プレイヤーを実装していきましょう。
まず、ゲーム上のプレイヤーのステートを考えます。

ファミコンなどのサッカーゲームでは、(今もかな)基本的には、 プレイヤー移動 → ボールの当たり判定 → ドリブルモード → シュート。 となっているので、こちらの形を参考に実装していきます。


主な処理として以下の関数を定義していきます。

ゲームループで、mainProcessを実行。

mainProcess() {

移動の際は、moveProcess()が実行される。

moveProcess() {

ボールをキープし、ドリブルモードの際は、keepProcess()が実行される。

keepProcess() {

シュートで、再びプレイヤー移動モード

shoot() {

と言ったところになります。

キーボード操作

キーボードで操作できるように、キーボードのイベントハンドラー 「keydown」と「keyup」 をbindし、取得していきます。
残念ながら、キーボードイベントのコールバックで取得できるキー情報は単体となってしまうので、複数入力されてもそのすべての情報は取得できません。
つまり、 右上移動の際に「右」と「上」のキーを押されても、最後に押された「上」のキーしか取得できないので、押されているキーの状態を保存する必要があります。

 
ビットマスクによる状態管理

そんなキー操作の状態管理に適しているのがビットマスクとなります。

enum Directionを作成します。それぞれの方向にヘキサリテラル(16進数表記)ビット割当を行います。(ここは好みかな)

export enum Direction {
    NONE = 0,
    UP = 0x0001,
    UP_RIGHT = 0x0003,
    RIGHT = 0x0002,
    BOTTOM_RIGHT = 0x0006,
    BOTTOM = 0x0004,
    BOTTOM_LEFT = 0x000c,
    LEFT = 0x0008,
    UP_LEFT = 0x0009,
}

押されている状態をstateのdirectionに保存します。

direction: 0x0000,

【押されているキーを追加】

押されているキーを追加するために、フラグを立てる時は、「|」論理和ORを用いてフラグをtrueにします。

上キー押下時

state.direction |= Direction.UP;

「keyup」イベントでフラグを解除

「keyup」イベントで押されているキーが離されたと判定し、キーフラグを解除します。
その際、 「~」論理否定NOTで反転させ、「&」論理積ANDを用いて論理積を求めます。

論理積用いたフラグ解除式

x & ~y

上キー押上時

state.direction &= ~Direction.UP;

こちらで、複数押されているキーの状態を保存しつつ、キーが離された時にそのキーのフラグの解除も行えました。

これらの状態をPlayerに渡し、ゲームループで移動させます。

コントローラー

ここまでやると、コントローラで移動させたくなってきましたね。
一家に一台あるPS5のコントローラーでも操作できるようにしていきましょう。
(PS5 本体持っていないが、コントローラーだけはある。。)

 

ゲームパッドAPI
ゲームパッドAPIを用いて実装していきます。
実装の詳細などは割愛させて頂くとし、MDNのドキュメントに実装方法などの詳細が記載されていますので、そちらを参考にして頂ければと思います。

MDN web docs

w3c Gamepad仕様

実装していて思ったのが、webkitGetGamepads、webkitGetGamepads()がどうも、TypeScriptで定義されなくなっているようで、ブラウザ側でも実装もされていないかもですね。(各ブラウザ、きちんと確認はしていないのでなんとも。)

: // : navigator.webkitGetGamepads
// ? navigator.webkitGetGamepads()

この辺りは削除しました。

ボール

ボールの表現をBall Classで実装していきます。

Playerと同様に、x座標、y座標、幅 width、高さ height、早さ speed、に加え、半径rを定義します。

interface BallProps {
    x: number;
    y: number;
    r: number;
    width: number;
    height: number;
    speed: number;
}
ボールの当たり判定

ボールの当たり判定をPlayer側に実装していきます。

 

プレイヤーとボールの2点間の距離

まず、プレイヤーとボールの2点間の距離を求めるための式として、 三平方の定理(ピタゴラスの定理) を用いて計算します。

「直角三角形において斜辺の長さをc、直角と隣り合う2辺の長さをa、 bとするとき & a^{2} + b^{2} = c^{2}$ が成り立つ。」

 

    $$a^{2} + b^{2} = c^{2}$$

 

といった公式が使えるので使用していきます。(思い出しました?)

 

この時、斜線cを求める場合、

    $$c = \sqrt{(a)^2+(b)^2}$$


こちらの公式が使えますので、こちらを用いて、プレイヤーとボールの位置関係を考えると、以下の図のように

$$(点Aから点Bの長さ)^{2} = (yの差)^{2} + (xの差)^{2}$$

となり、斜線c(距離)を求めるとなると、

$$点Aから点Bの長さ = \sqrt{(yの差)^{2} + (xの差)^{2}}$$

で、求められます。

これを、TypeScript(JavaScript)で実装すると以下の様になります。

PlayerのgetDistance関数

Math.sqrt(Math.pow(x - this.footX, 2) + Math.pow(y - this.footY, 2));
(this.footXなどは、足元に中心点を置いてます。)

 

BallCollider

上記の距離を用いて、ボールの当たり判定を求めます。

サッカーボールは円形なので、$(xの差)^{2} + (yの差)^{2}$が、「半径の2乗」の値より小さいときに当たりといった形で当たり判定をつけることができます。

$$(xの差)^2+(yの差)^2 < r^2$$

PlayerのcheckBallCollision関数

const dx = ball.x - this.x;
const dy = ball.y - this.y - this.height / 2;

if (dx * dx + dy * dy < ball.r * ball.r) {
    ...
ゴール判定をPub/Sub的にcallback

ボールがゴールに入ったという判定を、どう保持させようかと考えた時、Ball Classに保持するのが適切かなと思い、Ball Classに保持させました。
ゴールもPlayerやBall同様、Goal classとして抽象化されていて、x、y座標を持っています。

Filedも同様で、GoalとFiledはシングルトンとして扱っています。

isGoal(): boolean {
    const field: Filed = Filed.getInstance();
    const goal: Goal = Goal.getInstance();

    let isGoal = false;
    if (
        field.x > this.x + this.width &&
        field.y + goal.y < this.y + this.height &&
        field.y + goal.y + goal.height > this.y + this.height
    ) {
        isGoal = true;
        this.goalCallBack();
    }
    return isGoal;
}

この際、ボール自身がゴールに入ったと判定できていますが、それを知らせる為にコールバックが必要となります。goalCallBack関数を変数で定義し、

public goalCallBack: Function = () => {};
Pub/Sub的に扱えるように、addGoalCallBack関数で登録(購読)できるようにします。

addGoalCallBack(fn: Function) {
    this.goalCallBack = fn;
}

これで、ボール側からゴールの送信されると、スコアが加算されるようにコールバックを作成できます。

state.balls[i].addGoalCallBack(() => {
    state.score++;
    state.isGoalAnime = true;
});

Pub/Subは個人的に好き。

Vue3に適応

さあ。画面を作成していくためにVue3を実装していきましょう。
Vue.jsは1の時代から使用しているのですが、Vue3をちゃんと扱うのは何気に今回初かも。

Vue3になり、大きな変化として、 Options API → Composition API となり、更に、Vue.js 3.2 から <script setup> 構文が使える様になっております。

個人的には、ClassComponentのままでも慣れてて良かったのですけどね。

構築のためにviteを使用していきます。(詳細は割愛)

https://vitejs.dev/

以下のコマンドでプロジェクトを生成。

$ npm init vue@latest

これまでのVueとの違いとして、reactiveやonMountedメソッドなどが用意されていることですかね。

import { ref, reactive, onMounted, onUnmounted } from "vue";

ゲームの状態である stateをまるっとリアクティブ化させるため、reactiveメソッドを使用します。

let state = reactive({
    score: 0,
    direction: 0x0000,

あと、キーボードイベントの為のkeydownイベントリスナーなどは、onMountedメソッドに記述するのが正らしい。

onMounted(() => {

プレイヤーやらボールやらは、単一ファイルコンポーネント(SFC)として作成。
stateの値をそのままpropsでプロパティの受け渡しを行います。

<PlayerView
:x="state.player.x"
:y="state.player.y"
...
/>

SFC側で<script setup> 構文用いて受け取り、templateにbindさせれば完成。

<script setup lang="ts">
const props = defineProps<{
    x: Number;
    y: Number;
    ...
}>();
アニメーション

ボールがゴールした際にアニメーションが欲しいなと思い、CSSアニメーションで実装。


ボールのゴールのコールバックで、stateのisGoalAnimeをtrueにしアニメ開始。
animationendイベントでアニメ終了イベントを取得し、stateのisGoalAnimeをfalse。

従来であれば、DOM(アニメのcomponent)にイベントを貼るべきですが、まあこのくらいの規模ならと、ここは横着して雑にwindowにイベントを貼っちゃってます。

window.addEventListener("animationend", () => {
    if (state.isGoalAnime) state.isGoalAnime = false;
});
何個か問題も

SFCの<script setup> 構文内のpropsで「Cannot redeclare block-scoped variable」が発生する。

ThePlayer.vue

const props = defineProps<{ 

こちらは、「propsの変数は既に宣言されているので再宣言できないよ」というEslintのエラーぽく、SFCが別々のファイルと存在していても、ファイルが グローバルスコープを共有するスクリプトとして宣言している。 とTypeScriptで認識してしまい発生しちゃっているぽいです。
propsを定義している変数のスコープが、グローバルと認識してしまって、ファイルを分けても同様の変数名で宣言した場合にエラーが発生してしまう。

回避の為に、exportでモジュール化するなど (import先のファイルでdefault Vue.extend({})などの宣言)の回避策はあるようですが、そもそもSFC(<script setup> 構文)で作成しているのに exportしないといけないって何?(笑)と思い、そのままにしてしまいました。
これには、皆さんも悩まさせれているようで、回避策も他にも挙がっている感じでしたが、根本的な解決は待つしかなさそうな雰囲気でした。

もうひとつ。こちらも同様で、VScodeにVeturのプラグイン入れていますが、SFCをimportする際に「is not a module.Vetur(2306)」発生。回避のため、@ts-ignore付けてしまった。。。

// @ts-ignore
import GoalAnime from "./components/GoalAnime.vue";

is not a module.Vetur(2306)

完成

と言った感じで、今回の作成の大まかなところのみ触れてきましたが、取り敢えず完成しましたね。

あ。。ここまで読んで頂いた人は気がついたかもしれませんが、 render部分ですがVue.jsじゃなく、Canvasでもその他でも良かった訳で、Vue3が使いたかっただけで無理やり使用してみました。

しかしながら、renderをVue3(DOM)にする事によって、CSSでレイアウトできたり、アニメーションを作成したりと、メリットもそれなりにあるかと思います。

また、この様に、ViewとModelの分離が行えれば、容易にrenderも変更できたり、Testも書きやすかったりしますので、ViewとModelの分離は大事ですね。

最後に

バタバタ思いついて、仕事終わって帰って夜中にカチカチと2週間ほどで作成したこともあって、多々至らないところもあるのですが、スルーしちゃってしまっている感があります。。PlayerとBallのframeCount、currentFrameの扱いのブレや、ドリブル時にボールを追いかけるプレイヤーの向きによって近づく距離を手調整行いマジックナンバー発生しているのとか、プレイヤーの状態管理が甘く、ちょっとしたバグも発生しているので、このレベルでもステートマシーンを導入した方が良さげとかとかとか。。
でも、全部やりだしたらキリがないのでここで一旦終了としました。

しかしながら、久々にゲームぽいのを実装して楽しかったのと、(こんな事やってて良いのかという背徳感はありつつ(笑))以下の様にもっとこれやりたい欲も出てきたのは事実です。

  • ボール複数対応しているが、複数出すUIとかない。
  • やっぱキーパーいるんじゃない?
  • キーパーもいたら敵もいるんじゃない?
  • ボールの軌道が一直線なので、宙に浮いたり、カーブしたりさせたい
  • 敵も作るのであれば、サーバー建てて、2人でプレイできるようにしたい
  • そもそもドット絵がイケてないので、ゆっくり作成し直したい。。
  • サウンド、SE入れたくもなってきた

と、考えたらキリがないのですよね。。
取り敢えず、それなりにサッカーゲームのベースとしても使えるかと思いますので、改造されたい方などはGitHubよりフォークして頂き、ご自由に改造して頂ければ幸いです。また、プルリクもお待ちしております。

https://github.com/webcyou-org/2022-VIVA-JS-WorldCup

Unityで作成すればもっとサクッと作成できるんだろうなぁと思いつつ、メジャーなゲームエンジン以外で、ちょっとまた作成したい。という気持ちが芽生えてしまったので、別のゲームを今後使って行く可能性の高いFlutter(Drat)で作成しようかと考えている今日この頃です。

 

ではでは。良いクリスマスを。。。

Comment

Related Article

JavaScriptで、DOMを放り投げる処理

2024.07.27

2022 VIVA JS World Cup 開幕!! 〜 Vue3で作るサッカーゲーム 〜

2022.12.24

OAuthのフローを可視化できるツールを作ってみました。

2020.05.17

令和の時代に、JavaScriptで Shift-JISファイル作成 全銀データフォーマットに対応する。

2020.03.03

インターネットにて世論調査を行う「世論Web」サービスを始めてみました。

2020.01.31

年末のレトロゲーム熱の際、ファミコンソフト一覧パッケージ作ってました。

2020.01.24

あと10日で「jsdo.it」のサービスが終わってしまう!! ソースダウンロードまだの方は急げぇ〜!

2019.10.21

正規表現 先読み後読み 論理積

2019.07.28

「二段階認証?」という方も 5分で覚える パスワードレス WebAuthnのまとめ

2019.07.07

上級者向け JavaScript 問題集 「javascript-questions」日本語翻訳担当してます。

2019.06.22

CATEGORY LIST

LATEST NEWS

Mac VSCodeで、SFML C++開発環境を作る。

C++

2024.09.09

Rust-SDL2 examplesをすべて試す

Rust

2024.09.01

JavaScriptで、DOMを放り投げる処理

JavaScript

2024.07.27

Rustで創る MOS 6502 CPU その2

Rust

2024.07.23

Rustで創る MOS 6502 CPU その1

Rust

2024.07.19

汎用 3D mesh/model viewerを求め。と、簡単に、FBXファイルをglTF(glb)に変換ツールを求め。

C++

2024.06.06

M1 Macで、OpenGL GLUTを使ってコンパイルする

C

2024.04.27

Rust - Actix Web mongo ユーザー登録 JWT認証

Rust

2024.03.24

Rust - Actix Web JWT 認証認可 APIの作成

Rust

2024.02.25

Rust - Actix Web × JSON 静的ファイルをAPIで返却

Rust

2024.01.19

Rust - Actix Web × MongoDB環境をサクッと起動

Rust

2024.01.18

5分で学ぶ RustでWave Function Collapse (波動関数崩壊アルゴリズム)

Rust

2024.01.15

RANKING

Follow

SPONSOR

現在、掲載募集中です。



Links

About Us

WEBデザイナーの、WEBデザイナーによる、WEBデザイナーの為のサイト。「みんなで書こう!」と仲間を募ってみたが、結局書くのは自分だけとなってしまいました。日々のメモを綴っていきます。

Entry Profile

Graphical FrontEnd Engineer
- Daisuke Takayama

MAD CITY 北九州市で生まれ育つ。20代はバンド活動に明け暮れ、ふと「webデザイナーになりたい。」と思い、デジタルハリウッド福岡校入学。卒業後、数々の賞を受賞、web業界をざわつかせる。
現在、主に、ゲーム制作中心に港区六本木界隈で活動中。

FOLLOW US