如何用 React, TypeScript 寫遊戲?
1. React的優勢
-
資料驅動, 根據state或者props的變化 => 檢視的變化, 以前的方式往往是直接操作 DOM 實現, 觸發某事件使得元素移動程式碼類似如:
=> this.moveRight = () => { this.left += 8; this.draw(); } this.draw = () => { if(this.ele === null){ this.ele = document.createElement(`img`); this.ele.src = this.url; this.ele.style.width = this.width + `px`; this.ele.style.height = this.height + `px`; this.ele.style.position = `absolute`; app.appendChild(this.ele); } this.ele.style.left = this.left + `px`; this.ele.style.top = this.top + `px`; };複製程式碼
現在就友好很多
=> this.moveRight = () => { this.setState( preState => ( { left: preState.left + 8 } )); } <ContraBG left={left} top={top} status={status} toward={toward}> </ContraBG>複製程式碼
-
結構更清晰, 逐個書寫需要渲染的元件, 能讓人一目瞭然的知道遊戲執行中載入的元件, 老的方式程式碼風格去渲染一個元素如
=> const plane = new ourplane(); plane.draw();複製程式碼
如果渲染的多了結構複雜了,閱讀就會十分困難。現在的程式碼風格就能夠一目瞭然的看到所有執行的元件
=> @observer class InGame extends React.PureComponent<InGameProps, {}> { render() { const { store } = this.props; return ( <InGameBG // 包裹元件負責渲染背景變化相關 store={store}> <Contra // 玩家控制的角色元件 store={store}/> <BulletsMap // 負責渲染子彈 store={store}/> <EnemiesMap // 負責渲染敵方角色 store={store}/> </InGameBG> ); } }複製程式碼
2. React的劣勢
-
靈活性
前者類與類之間繼承會靈活很多, 如飛機繼承至飛行物 => 飛行物繼承至動態物 => 動態物繼承至某一特性物體複製程式碼
其中子彈也可以繼承至飛行物使得飛行物等可以衍生更多子類。React中各元件只能繼承至React.Component,可採用HOC高階元件思想去渲染一系列具有相似性質的元件。如超級瑪麗遊戲中有許多的牆,它們具有相似的渲染邏輯,以及一些都會需要用到的方法, 可以通過寫一個靜態方塊的高階元件去生成, 能夠更高效的管理程式碼。
=> function WithStaticSquare<TOwnProps>(options: StaticSquareOption):ComponentDecorator<TOwnProps> { return Component => class HocSquare extends React.Component<TOwnProps, HocSquareState> { // xxx render() { const { styles, className } = this.state; const passThroughProps: any = this.props; const classNames = className ? `staticHocWrap ${className}` : "staticHocWrap"; const staticProps: WrappedStaticSquareUtils = { changeBackground: this.changeBackground, toTopAnimate: this.toTopAnimate }; // 提供一些可能會用到的改變背景圖的方法以及被撞時呼叫向上動畫的方法 return ( <div className={classNames} style={styles}> <Component hoc={staticProps} {...passThroughProps}/> </div> ); } } }複製程式碼
3. 效能問題
- 避免卡頓 前者直接操作某個DOM渲染不會有太多卡頓現象發生
React使用Mobx, Redux等進行整個遊戲資料控制時, 如果不對渲染進行優化, 當store某個屬性值變化導致所有接入props的元件都重新渲染一次代價是巨大的!
-
採用PureComponent某些元件需要這樣寫
=> class Square extends React.PureComponent<SquareProps, {}> { // xxx }複製程式碼
其中就需要了解PureComponent。React.PureComponent是2016.06.29 React 15.3中釋出。
PureComponent改變了生命週期方法shouldComponentUpdate,並且它會自動檢查元件是否需要重新渲染。這時,只有PureComponent檢測到state或者props發生變化時,PureComponent才會呼叫render方法,但是這種檢查只是淺計較這就意味著巢狀物件和陣列是不會被比較的更多資訊
-
多采用元件去渲染, 對比兩種方法
=> // 方法1. <InGameBG // 包裹元件負責渲染背景變化相關 store={store}> <Contra // 玩家控制的角色元件 store={store}/> <BulletsMap // 負責渲染子彈 store={store}/> <EnemiesMap // 負責渲染敵方角色 store={store}/> </InGameBG> //方法2. <InGameBG store={store}> <Contra store={store}/> <div> { bulletMap.map((bullet, index) => { if ( bullet ) { return ( <Bullet key={`Bullet-${index}`} {...bullet} index={index} store={store}/> ); } return null; }) } </div> <EnemiesMap store={store}/> </InGameBG>複製程式碼
這兩種方法的區別就是在於渲染子彈是否通過元件渲染還是在父元件中直接渲染, 其中方法2的效能會有很大的問題, 當某個子彈變化時使得最大的容器重新渲染, 其中所有子元件也會去判斷是否需要重新渲染,使得介面會出現卡頓。而方法1則只會在發生資料變化的子彈去渲染。
4. 需要注意的點
-
及時移除監聽, 在元件解除安裝時需要移除該元件的事件監聽, 時間函式等。如遊戲開始元件
=> class GameStart extends React.Component<GameStartProps, {}> { constructor(props) { super(props); this.onkeydownHandle = this.onkeydownHandle.bind(this); } componentDidMount() { this.onkeydown(); } componentWillUnmount() { this.destroy(); } destroy(): void { console.log("遊戲開始! GameStart Component destroy ...."); window.removeEventListener("keydown", this.onkeydownHandle); } onkeydownHandle(e: KeyboardEvent): void { const keyCode: KeyCodeType = e.keyCode; const { store } = this.props; const { updateGameStatus } = store; switch ( keyCode ) { case 72: updateGameStatus(1); break; } } onkeydown(): void { window.addEventListener("keydown", this.onkeydownHandle); } render() { return ( <div className="gameStartWrap"> </div> ); } }複製程式碼