はじめに
この春からReactエンジニアになった者です。
今まではC#とかUnityを触っていました。
Reactの練習として、個人制作で8番ライクなゲーム『八幡鉄道』を作りました。
作ったもの
『八幡鉄道』というゲームです。
Github Pagesで公開しているので、下のリンクから遊べます。
https://usuisio.github.io/yahata-railway/
ソースコードはこちらです。
https://github.com/Usuisio/yahata-railway
(補足:8番ライクって?)
KOTAKE CREATE様が制作したゲーム『8番出口』のシステムをオマージュした作品群です。
「何度も同じ場面を繰り返して異変を探す」「異変を見つけたら引き返す」というシンプルなルールが特徴です。
今回はReactを使って、8番出口のルールをWebページ上で再現しました。
感想
Reactハチャメチャに楽しいですね。。。
とにかく画面が出てくるまでが速いのがいい。
HTMLっぽく書くだけ。手軽。
コードの量と画面の完成度が比例するから、わかりやすく作る喜びを感じられる。
設計思想もすごくわかりやすい。UI要素はコンポーネントに分割して、ロジックはHooksで書く!
オブジェクト指向の世界にいながら最後までクラスの適切なサイズがわからなかった人間からすると、すごくシンプルで馴染みやすい概念だと感じます。
プロジェクト構成
使用したツール、ライブラリ、パッケージなどを書きます。
React
画面を作成するのに使いました。
Styled-Component
スタイルの記述に使いました。実際の現場で何を使うかが決まっていなかったので、CSS Modulesと比較して好みで決めました。
Material UI
https://mui.com/material-ui/
UIに多用しました。安心感がある。
Redux
状態管理に使用しました。今回で言うと「いま何ページ目?」「今回のページの異変は何?」という情報を管理しています。
react-i18next
ローカライズで使用しました。これ以外を知らない。
実装内容について
今回一番工夫したのが、画面を無限ループさせる実装です。
8番ライクの特徴として、「進んでも進んでも同じ場面が繰り返される」というものがあります。
今回はそれをReact = Webページで再現するために、スクロールしてもスクロールしても同じ内容が表示される……という実装を目指しました。
最終的に、「特定の領域までスクロールしたら上にスクロール位置をワープする」という手法で、あたかも無限にスクロールできるような仕組みにしました。
説明します。
ループ実装 -仕組み解説-
これが八幡鉄道の画面全体を表した図だと思ってください。
上から、ヘッダー、報告ボタン、ホームページ。一番下の黄色が判定用Div(実際には無色透明)です。
赤の二重枠が今のスクロール位置、つまりプレイヤーが見えている範囲です。
スクロールをどんどん下げていって、判定用Div(実際には目に見えないです)が少しでもモニターの表示範囲内に入ると……
スクロール位置をワープさせて上に戻します。
これで、見かけ上はホームページが何度も何度も繰り返されるようになっています。
ループ実装 -コード解説-
以下は説明用のコードなので、実際のものとは異なります。
export const GameLoop = () => {
// ホームページを格納するためのRef要素です
const pageContainerRef = useRef<HTMLDivElement | null>(null);
// 判定用Divを格納するためのRef要素です
const targetRef = useRef<HTMLDivElement | null>(null);
// IntersectionObserverという仕組みを使って、
// ある要素が画面の見えている範囲に入ってきたかをチェックします
const observerPageContainer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
// entryが画面内にある時、
// window.scrollToでスクロール位置を動かします。
// pageContainerRef.current.offsetTopでスクロールの行き先を指定してます
pageContainerRef.current &&
window.scrollTo(0, pageContainerRef.current.offsetTop);
}
},
{
root: null,
rootMargin: "0px",
threshold: 0.1,
}
);
// targetRef(判定用Div)を先ほど作ったobserverにセットしています
useEffect(() => {
if (targetRef.current) {
observer.observe(targetRef.current);
}
return () => {
if (targetRef.current) {
observer.unobserve(targetRef.current);
}
};
}, [currentTrick]);
return (
<>
<Header/>
<ReportButton/>
<div ref={pageContainerRef}>
<HomePage/>
</div>
{/*判定用Div*/}
<div
ref={targetRef}
style={{
width: "100px",
height: "100px",
/>
</>
);
};
今後の課題
ゲームの完成を目指しすぎてかなり読みづらいコードになってしまったので、どこかでリファクタしたいです。
せっかくなので、container/presenterパターンを意識してやってみたい。
あとテストコードも一切書いていないので、次からは単体テストを交えながら書きたいですね……
以上!
お読みいただきありがとうございました。
良ければゲームもやってみてね。
https://usuisio.github.io/yahata-railway/