🍞
TypeScript で実装した Robots を React でブラウザゲームにしてみた (やっとGUI ver.)
React 完全に理解した (大嘘)
はじめに
- 個人的な React 学習の題材として、過去記事 [1] [2] のCUI/ターミナル上で動くゲーム (Robots) をブラウザゲーム化
- react-hooks の学習のため関数コンポーネントで作成
- ブラウザゲームは GitHub Pages にて公開
Before (CUI/ターミナル)
ターミナル上でキーボード操作
After (GUI/ブラウザ)
ブラウザ上でマウス操作するゲーム
コンポーネント
- React のコンポーネントでは
Props
とState
で UI の状態や情報を管理 -
Props
及びState
の詳細は下記の公式ドキュメントを参照
コンポーネント構成
- Game : ゲーム全体(トップ階層)で全体をまとめるコンポーネント
- Board : 盤面のコンポーネント
- Square : 盤面を構成する一マスのコンポーネント
- Control : プレイヤーロボットを操作するコンポーネント
- Info : レベル、スコア、ステータスのゲーム情報を表示するコンポーネント
Game
components/Robots.ts
はここを参照
※過去記事の CUI バージョンのロジックを修正して使用
Game.tsx
import React, { useState } from "react";
import Control from "components/Control";
import Board from "components/Board";
import Info from "components/Info";
import {
width,
height,
RobotInfo,
RobotMove,
init_robots,
move_robots,
is_wipeout,
check_gameover,
count_total_dead_enemy,
calc_bonus,
} from "components/Robots";
export type GameProps = {
robotList: RobotInfo[];
};
const Game = (props: GameProps) => {
const [level, setLevel] = useState<number>(1);
const [score, setScore] = useState<number>(0);
const [robotList, setRobotList] = useState<RobotInfo[]>(
init_robots(props.robotList, level)
);
//playerが動いたときに描画させるためのupdate/setUpdate
const [update, setUpdata] = useState<boolean>(false);
// status
let status = "Game is ongoing";
if (check_gameover(robotList)) {
status = "Game Over...";
}
//Game Clear
let bonus = calc_bonus(level);
if (is_wipeout(robotList)) {
// initialize board for next level
setLevel(level + 1);
setScore(count_total_dead_enemy(robotList, level + 1) * 10 + bonus);
setRobotList(init_robots(robotList, level + 1));
bonus = calc_bonus(level + 1);
}
const submit = (move: RobotMove) => {
// Game Overのときは入力を受け付けない
if (status === "Game Over...") return;
// ロボットの動きで他のコンポーネントを強制的に描画させる
// robotList,setRobotListの持たせ方に問題?
setUpdata(update ? false : true);
setRobotList(move_robots(robotList, move));
setScore(count_total_dead_enemy(robotList, level) * 10 + bonus);
};
return (
<div className="game">
<div className="game-board">
<Board robotList={robotList} width={width} height={height} />
</div>
<div className="game-info">
<Control onClick={submit} />
<Info level={level} score={score} status={status} />
</div>
</div>
);
};
export default Game;
Board
interfaces.ts
export type ISquare = "@" | "+" | "*" | null;
Board.tsx
import React from "react";
import Square from "components/Square";
import { ISquare } from "components/interfaces";
import { RobotInfo, RobotType } from "components/Robots";
interface BoardProps {
width: number;
height: number;
robotList: RobotInfo[];
}
function put_robot(robotList: RobotInfo[], x: number, y: number): ISquare {
// forEach/find の中でreturnは使ってはいけない?
// https://www.hanachiru-blog.com/entry/2019/10/31/154305
let val: ISquare = null;
robotList.find((element) => {
if (element.x === x && element.y === y) {
switch (element.type) {
case RobotType.Player:
val = "@";
break;
case RobotType.Enemy:
val = "+";
break;
case RobotType.Scrap:
val = "*";
break;
default:
val = null;
}
}
});
return val;
}
const Board = (props: BoardProps) => {
return (
<div>
{[...Array(props.height)].map((_, i) => {
return (
<div className="board-row" key={i}>
{[...Array(props.width)].map((_, j) => {
return (
<Square value={put_robot(props.robotList, j, i)} key={j} />
);
})}
</div>
);
})}
</div>
);
};
export default Board;
Square
Square.tsx
import React from "react";
import { ISquare } from "components/interfaces";
interface SquareProps {
value: ISquare;
}
const Square = (props: SquareProps) => {
return <button className="square">{props.value}</button>;
};
export default Square;
Control
Control.tsx
import React from "react";
import "../index.css";
import { RobotMove } from "components/Robots";
interface ControlProps {
onClick: Function;
}
const Control = (props: ControlProps) => {
return (
<React.Fragment>
<label>
<h4>Move:</h4>
</label>
<div>
<button
type="submit"
onClick={(e) => props.onClick(RobotMove.UpperLeft)}
>
y
</button>
<button type="submit" onClick={(e) => props.onClick(RobotMove.Up)}>
k
</button>
<button
type="submit"
onClick={(e) => props.onClick(RobotMove.UpperRight)}
>
u
</button>
</div>
<div>
<button type="submit" onClick={(e) => props.onClick(RobotMove.Left)}>
h
</button>
<button type="submit" onClick={(e) => props.onClick(RobotMove.Wait)}>
w
</button>
<button type="submit" onClick={(e) => props.onClick(RobotMove.Right)}>
l
</button>
</div>
<div>
<button
type="submit"
onClick={(e) => props.onClick(RobotMove.LowerLeft)}
>
b
</button>
<button type="submit" onClick={(e) => props.onClick(RobotMove.Down)}>
j
</button>
<button
type="submit"
onClick={(e) => props.onClick(RobotMove.LowerRight)}
>
n
</button>
</div>
<div>
<button
type="submit"
onClick={(e) => props.onClick(RobotMove.Teleport)}
>
t
</button>
</div>
</React.Fragment>
);
};
export default Control;
Info
Info.tsx
import React, { Fragment } from "react";
type InfoProps = {
level: number;
score: number;
status: string;
};
const Info = (props: InfoProps) => {
return (
<React.Fragment>
<label>
<h4>Legend:</h4>
</label>
<div>
<label>+ : robot</label>
</div>
<div>
<label>* : junk heap</label>
</div>
<div>
<label>@ : you</label>
</div>
<label>
<h4>Level & Score:</h4>
</label>
<div>
<label>level : {props.level}</label>
</div>
<div>
<label>score : {props.score}</label>
</div>
<label>
<h4>Status:</h4>
</label>
<div>
<label>{props.status}</label>
</div>
</React.Fragment>
);
};
export default Info;
ソース
おわりに
- Reactの基本の基はわかった(ということにしておく・・・)
- しかし、まだ
useState
を上手く使えていない気もする (というよりも設計が上手くできてない?) - 実は three.js を使って派手に仕立てる野望があったが断念[3]
- 誰かまともな CSS をあててほしい・・・上記の GitHub で PR 受付中 (他力本願)
Discussion