Next.jsでゲーム作る (3)
webの沼に生きるものとして一度はゲーム制作をやりたいと思った今日この頃.
Next.jsを愛してやまないので,「Next.jsはやればできる子なんだぞ」というところを見せる意味で作ってみようと思います.
前回の記事はこちらからどうぞ
stateの保持
ReactやReduxのstateは再レンダリングした場合初期値にリセットされてしまうため,今回のようなゲームでは一時中断すると最初からになってしまいます.そこで,逐次的にstateをlocalstorageIndexedDBに保存することでstateの永続化を図ります.
2022/10/13 追記
localstorageでは最大5MB程度しか保存できないためIndexedDBに保存することにしました.
npm install redux-persist @piotr-cz/redux-persist-idb-storage
/libs/redux/store.ts
+ import { useSelector as rawUseSelector, TypedUseSelectorHook } from 'react-redux';
+ import {
+ persistStore,
+ persistReducer,
+ FLUSH,
+ REHYDRATE,
+ PAUSE,
+ PERSIST,
+ PURGE,
+ REGISTER,
+ } from 'redux-persist'
+ import storage from './storage';
const persistRootConfig = {
key: 'root',
storage,
};
const rootReducers = combineReducers({
scene: sceneReducer,
mode: modeReducer,
})
export const store = configureStore({
reducer: persistReducer<ReturnType<typeof rootReducers>>(persistRootConfig, rootReducers),
devTools: process.env.NODE_ENV !== 'production',
middleware: (getDefaultMiddleware) => getDefaultMiddleware({
serializableCheck: {
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER]
}
})
});
export const persistor = persistStore(store)
export type RootState = ReturnType<typeof store.getState>;
export const useSelector: TypedUseSelectorHook<RootState> = rawUseSelector;
_app.tsx
+ import { persistor, store } from '../libs/redux/store';
+ import { PersistGate } from 'redux-persist/integration/react'
function MyApp({ Component, pageProps }: AppProps) {
return (
<Provider store={store}>
+ <PersistGate loading={null} persistor={persistor}>
<Component {...pageProps} />
+ </PersistGate>
</Provider>
);
}
サーバーにはlocalstorageがないのでstorage
は自作します.
/libs/redux/storage.ts
import createIdbStorage from '@piotr-cz/redux-persist-idb-storage'
const createNoopStorage = () => {
return {
getItem(_key: string) {
return Promise.resolve(null);
},
setItem(_key: string, value: string) {
return Promise.resolve(value);
},
removeItem(_key: string) {
return Promise.resolve();
},
};
};
const storage = typeof window !== "undefined"
? createIdbStorage({ name: 'TrafficLtd', storeName: 'TrafficLtd' })
: createNoopStorage();
export default storage;
客を待たせる
今回のゲームではすべてreduxで管理するため,客についての情報も同様です.
まず,各地点のsliceを作成します.stateには地点名をkeyとして客数customers
,座標point
,表示/非表示show
をvalueにもたせます.reducerとしては地点を表示するshowTrainPoint
,客を追加するaddTrainCustomers
を書きます.また,ゲームリセットのことを考えてclearTrainPoints
も用意しました.
PointTypeの定義
export type PointType<K extends string> = {
[key in K]: {
customers: number
point: number[]
show: boolean
}
}
電車についてのsliceはこちら.これに限らず地点数が多いとメモリオーバーフローが発生するかもしれないのでいくつかに分割する予定です.(特にバス停は死ぬほどあるので...)
/libs/redux/slices/trainSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { PointType } from './companySlice';
import train from '../../../assets/train/N02-21_Station.json'
export type TrainKey = keyof typeof train.points
export const trainKeys = Object.keys(train.points) as TrainKey[]
const initialState: PointType<TrainKey> = Object.entries(train.points).reduce((acc, cur) => {
acc[cur[0] as TrainKey] = { customers: 0, point: cur[1], show: false }
return acc
}, {} as PointType<TrainKey>)
export const trainSlice = createSlice({
name: 'train',
initialState: initialState,
reducers: {
showTrainPoint: (state, action: PayloadAction<TrainKey>) => {
state[action.payload].show = true
},
addTrainCustomers: (state, action: PayloadAction<{ name: TrainKey, count: number }>) => {
state[action.payload.name].customers += action.payload.count
},
clearTrainPoints: () => initialState
}
});
export const { showTrainPoint, addTrainCustomers, clearTrainPoints } = trainSlice.actions;
export default trainSlice.reducer;
次に,全体の客の待ち人数を把握するため会社のsliceも作ります.
/libs/redux/slices/companySlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
type CompanyInitialStateType = {
customers: number
}
const initialState: CompanyInitialStateType = {
customers: 0
}
export const companySlice = createSlice({
name: 'company',
initialState: initialState,
reducers: {
addCustomers: (state, action: PayloadAction<number>) => {
state.customers += action.payload
},
clearCompany: () => initialState
}
});
export const { addCustomers, clearCompany } = companySlice.actions;
export default companySlice.reducer;
ゲーム進行に必要なシステムも作成します.
/libs/redux/slices/systemSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { AirportKey } from './airportSlice';
import { PointType } from './companySlice';
import { HokkaidoBusKey } from './hokkaidoBusSlice';
import { PortKey } from './portSlice';
import { TrainKey } from './trainSlice';
export type PlaceKey = AirportKey | HokkaidoBusKey | PortKey | TrainKey
type SystemInitialStateType = {
time: number,
point: PointType<PlaceKey>
}
const initialState: SystemInitialStateType = {
time: 0,
point: {} as PointType<PlaceKey>,
}
export const systemSlice = createSlice({
name: 'system',
initialState: initialState,
reducers: {
selectPoint: (state, action: PayloadAction<PointType<PlaceKey>>) => {
state.point = action.payload
},
setTime: (state, action: PayloadAction<number>) => {
state.time = action.payload
},
clearSystem: () => initialState
}
});
export const { selectPoint, setTime, clearSystem } = systemSlice.actions;
export default systemSlice.reducer;
次に,客の情報をもつsliceを作ります.
/libs/redux/slices/customerSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import cuid from 'cuid';
import { PlaceKey } from './systemSlice';
export type CustomerType = {
[id: string]: {
money: number
place: PlaceKey | 'flying'
paths: PlaceKey[]
}
}
const initialState: CustomerType = {}
export const customerSlice = createSlice({
name: 'customer',
initialState: initialState,
reducers: {
addCustomer: (state, action: PayloadAction<{ place: PlaceKey | 'flying', paths: PlaceKey[] }>) => ({
...state,
[cuid()]: { money: 0, place: action.payload.place, paths: action.payload.paths }
}),
updatePaths: (state, action: PayloadAction<string>) => {
if (state[action.payload].place === state[action.payload].paths[0]) state[action.payload].paths.shift()
},
clearCustomers: () => initialState
}
});
export const { addCustomer, updatePaths, clearCustomers } = customerSlice.actions;
export default customerSlice.reducer;
storeも追加します.
/libs/redux/store.ts
+ import airportReducer from './slices/airportSlice';
+ import hokkaidoBusReducer from './slices/busSlice';
+ import trainReducer from './slices/trainSlice';
+ import portReducer from './slices/portSlice';
+ import companyReducer from './slices/companySlice';
+ import systemReducer from './slices/systemSlice';
+ const persistAirportConfig = {
+ key: 'airport',
+ storage
+ }
+ const persistHokkaidoBusConfig = {
+ key: 'hokkaidoBus',
+ storage
+ }
+ const persistTrainConfig = {
+ key: 'train',
+ storage
+ }
+ const persistPortConfig = {
+ key: 'port',
+ storage
+ }
const reducers = combineReducers({
scene: sceneReducer,
mode: modeReducer,
+ airport: persistReducer(persistAirportConfig, airportReducer),
+ train: persistReducer(persistTrainConfig, trainReducer),
+ hokkaidoBus: persistReducer(persistHokkaidoBusConfig, hokkaidoBusReducer),
+ port: persistReducer(persistPortConfig, portReducer),
+ company: companyReducer,
+ system: systemReducer
})
次に,時間経過で客を追加していきたいのでtimerパッケージを入れます.自前で作ってもいいけどレンダリング地獄が怖いので...
npm install react-timer-hook
/scenes/play.tsx
+ import { useEffect } from 'react';
+ import useCompany from '../libs/redux/hooks/useCompany';
+ import useSystem from '../libs/redux/hooks/useSystem';
+ import { useStopwatch } from 'react-timer-hook';
const PlayScene = () => {
+ const { company, addCustomers } = useCompany();
+ const { system, setTime } = useSystem()
+ const offset = new Date()
+ offset.setSeconds(system.time + offset.getSeconds())
+ const { days, hours, minutes, seconds, isRunning } = useStopwatch({ autoStart: true, offsetTimestamp: offset })
+ useEffect(() => {
+ if (isRunning && seconds % 10 === 0) addCustomers(1)
+ }, [isRunning, seconds, addCustomers])
...
}
これで一定時間ごとにincrementするtimerができました.これを各地点で行えば客を待たせることができます.
次回は時間経過とともにマーカーと客を増やしていくとともに,システムのstateも詳しく作っていきます.
参考文献
Discussion