如何用 React, TypeScript 寫游戲?
image
1. React的優(yōu)勢(shì)
- 數(shù)據(jù)驅(qū)動(dòng), 根據(jù)state或者props的變化 => 視圖的變化, 以前的方式往往是直接操作 DOM 實(shí)現(xiàn), 觸發(fā)某事件使得元素移動(dòng)代碼類似如:
=>
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';
};
現(xiàn)在就友好很多
=>
this.moveRight = () => {
this.setState( preState => (
{
left: preState.left + 8
}
));
}
<ContraBG
left={left}
top={top}
status={status}
toward={toward}>
</ContraBG>
- 結(jié)構(gòu)更清晰, 逐個(gè)書(shū)寫需要渲染的組件, 能讓人一目了然的知道游戲運(yùn)行中加載的組件, 老的方式代碼風(fēng)格去渲染一個(gè)元素如
=>
const plane = new ourplane();
plane.draw();
如果渲染的多了結(jié)構(gòu)復(fù)雜了,閱讀就會(huì)十分困難。現(xiàn)在的代碼風(fēng)格就能夠一目了然的看到所有運(yùn)行的組件
=>
@observer
class InGame extends React.PureComponent<InGameProps, {}> {
render() {
const { store } = this.props;
return (
<InGameBG // 包裹組件負(fù)責(zé)渲染背景變化相關(guān)
store={store}>
<Contra // 玩家控制的角色組件
store={store}/>
<BulletsMap // 負(fù)責(zé)渲染子彈
store={store}/>
<EnemiesMap // 負(fù)責(zé)渲染敵方角色
store={store}/>
</InGameBG>
);
}
}
2. React的劣勢(shì)
-
靈活性
前者類與類之間繼承會(huì)靈活很多, 如
飛機(jī)繼承至飛行物 => 飛行物繼承至動(dòng)態(tài)物 => 動(dòng)態(tài)物繼承至某一特性物體
其中子彈也可以繼承至飛行物使得飛行物等可以衍生更多子類垒在。React中各組件只能繼承至React.Component,可采用HOC高階組件思想去渲染一系列具有相似性質(zhì)的組件栗精。如超級(jí)瑪麗游戲中有許多的墻,它們具有相似的渲染邏輯,以及一些都會(huì)需要用到的方法, 可以通過(guò)寫一個(gè)靜態(tài)方塊的高階組件去生成, 能夠更高效的管理代碼撒犀。
=>
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
}; // 提供一些可能會(huì)用到的改變背景圖的方法以及被撞時(shí)調(diào)用向上動(dòng)畫的方法
return (
<div
className={classNames}
style={styles}>
<Component
hoc={staticProps}
{...passThroughProps}/>
</div>
);
}
}
}
3. 性能問(wèn)題
-
避免卡頓 前者直接操作某個(gè)DOM渲染不會(huì)有太多卡頓現(xiàn)象發(fā)生
React使用Mobx, Redux等進(jìn)行整個(gè)游戲數(shù)據(jù)控制時(shí), 如果不對(duì)渲染進(jìn)行優(yōu)化, 當(dāng)store某個(gè)屬性值變化導(dǎo)致所有接入props的組件都重新渲染一次代價(jià)是巨大的!
- 采用PureComponent某些組件需要這樣寫
=>
class Square extends React.PureComponent<SquareProps, {}> {
// xxx
}
其中就需要了解PureComponent。React.PureComponent是2016.06.29 React 15.3中發(fā)布副渴。
image
PureComponent改變了生命周期方法shouldComponentUpdate解愤,并且它會(huì)自動(dòng)檢查組件是否需要重新渲染骤素。這時(shí)家淤,只有PureComponent檢測(cè)到state或者props發(fā)生變化時(shí)异剥,PureComponent才會(huì)調(diào)用render方法,但是這種檢查只是淺計(jì)較這就意味著嵌套對(duì)象和數(shù)組是不會(huì)被比較的更多信息
- 多采用組件去渲染, 對(duì)比兩種方法
=>
// 方法1.
<InGameBG // 包裹組件負(fù)責(zé)渲染背景變化相關(guān)
store={store}>
<Contra // 玩家控制的角色組件
store={store}/>
<BulletsMap // 負(fù)責(zé)渲染子彈
store={store}/>
<EnemiesMap // 負(fù)責(zé)渲染敵方角色
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>
這兩種方法的區(qū)別就是在于渲染子彈是否通過(guò)組件渲染還是在父組件中直接渲染, 其中方法2的性能會(huì)有很大的問(wèn)題, 當(dāng)某個(gè)子彈變化時(shí)使得最大的容器重新渲染, 其中所有子組件也會(huì)去判斷是否需要重新渲染瑟由,使得界面會(huì)出現(xiàn)卡頓絮重。而方法1則只會(huì)在發(fā)生數(shù)據(jù)變化的子彈去渲染。
4. 需要注意的點(diǎn)
- 及時(shí)移除監(jiān)聽(tīng), 在組件卸載時(shí)需要移除該組件的事件監(jiān)聽(tīng), 時(shí)間函數(shù)等歹苦。如游戲開(kāi)始組件
=>
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("游戲開(kāi)始! 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>
);
}
}
5. 最近寫的 超級(jí)魂斗羅 效果與GitHub
超級(jí)魂斗羅
超級(jí)魂斗羅
https://github.com/xiaoxiaojx/SuperContra