React Hooks + Typescript实现贪吃蛇小游戏

项目地址(Github)
React Hooks + Typescript实现贪吃蛇小游戏_第1张图片

初始化项目

react 16.8已经正式支持react hooks的使用,开发者可以通过官方推荐的create react app创建一个基于typescript + react16.8(hooks)的初始化项目模板(直接根据引导创建即可,无需额外的初始化配置)。

潜在的问题:使用npm进行安装可能带来一些bug,这些bug目前还没有被修复。如果npm i之后使用npm start无法启动项目。可以尝试删除node_modules之后使用yarn安装依赖。

rm -r node_modules
yarn install

设计“蛇”组件

一条蛇应当包含自己独立的状态,如长度,位置,前进方向等信息。也要包含一些受控信息,如速度等。这两类信息的区分界限比较模糊,开发时也可以根据个人爱好进行选择。

但一个合理的区分方式是,自身状态(state)应当与外界属性无关或不完全相关。
f : ( p r o p s , p r o p s s i d e ) → s t a t e f: (props, props_{side}) \to state f:(props,propsside)state
其中props为组件的可控参数, p r o p s s i d e props_{side} propsside为组件的副作用参数,如当前的游戏时长。基于此种理解既可以将游戏时长这种相对隐形的参数作为显性参数与其他参数等价都作为props设计也可以作为组件的内部状态,甚至还可以构建一个中间层,时间作为中间层组件的state,作为最下层组件的props。

使用react hooks,组件的定义可以写作如下的形式

const Snake: React.FC<SnakeProps> = (props: SnakeProps) => {}

props

// index.d.ts
type BBox = [[number, number], [number, number]];

interface SnakeProps {
  playground: BBox;
  mode: 'easy' | 'medium' | 'hard' | 'crazy'
}

// snake.ts FC
const { playground, mode } = props;

state

const initSpace: SnakeSpace = [
    [
      Math.round((playground[0][0] + playground[1][0]) / 2),
      Math.round((playground[0][1] + playground[1][1]) / 2)
    ]
  ];
  initSpace.push([initSpace[0][0] + 1, initSpace[0][1]])
  const [space, setSpace] = useState(initSpace);
  const [direction, setDirection] = useState(moveTo.top);
  const initFood: position = throwFood(space, playground);
  const [food, setFood] = useState(initFood);

问题:useState(initFood)的返回值是根据initFood进行推断的,这时,如果不给于明确的类型定义,而是直接给与一个初始值,可能导致类型推断不严谨或与期望的类型不等价从而带来后续的一些类型问题。

这里采用的方式是抛弃了直接给初始值的方式,而是将初始值放在一个带有我们定义好的初始类型的变量中,从而通过该变量的类型,将准确的类型信息传递给useState,使得得到的返回值food的类型与initFood的类型一致。

然而,这种方式相对更为hack一些,也使得代码的结构因为为了满足类型开发的需要而变得冗余,这便违背了使用ts的初衷。这里可以使用泛型的方式定义food的类型。

const [food, setFood] = useState<position>(throwFood(space, playground));

触发-状态变化

蛇组件本身的state是由一些 p r o p s s i d e props_{side} propsside触发的。我这里在设计时主要考虑了两种情况:

  • 时间:蛇的状态是随时间的函数,这里设置一个定时器来根据每一个新的时间更新状态。
  • 全局事件:不与某个组件绑定的交互事件(如按钮点击),需要全局捕获(如键盘控制)。这里主要是蛇的前进方向。

时间状态

useEffect(() => {
    const round = setInterval(() => {
      setSpace((space) => {
        if (die(space, playground)) {
          clearRound();
          console.log('die!!');
          return [];
        }
        let newSpace: SnakeSpace;
        newSpace = [
          add(space[0], direction),
          ...space.slice(0, -1)
        ];
        if (eatFood(space, food)) {
          newSpace.push(space[space.length - 1]);
          console.log('eat', space, newSpace)
          setFood(throwFood(newSpace, playground));
        }
        return newSpace;
      });
    }, modeSpeed[mode]);
    const clearRound = () => {
      clearInterval(round);
      setSpace(initSpace);
    }
    return () => {
      console.log('unmount')
      clearInterval(round);
    }
  }, [direction, food, mode]);

键盘控制

这里直接使用js原生的addEventListener来监听发生在document上的keydown事件。由于事件监听的定义是发生在整个游戏全部的生命周期中的,可以理解为一个常量监听器,游戏推出前不会随任何值变化,因此,在useEffect的第二个参数(依赖的状态变量),可以设为空(数组),表示这里的effect不会随任何变量的更新而更新,而是在组件挂载到注销全周期内都不会发生变化。

useEffect(() => {
    const keyMap: { [key: string]: string } = {
      A: 'left',
      W: 'top',
      S: 'bottom',
      D: 'right'
    }
    const keyHandler = (e: KeyboardEvent) => {
      let key = String.fromCharCode(e.keyCode);
      if (key in keyMap) {
        setDirection(moveTo[keyMap[key]]);
      }
    }
    document.addEventListener('keydown', keyHandler)
    return () => {
      document.removeEventListener('keydown', keyHandler);
    }
  }, [])

对于键盘上的事件,会存在一个keyCode,这里可以使用String.fromCharCode()将keyCode转化为对应的字符(根据ascii)。

你可能感兴趣的:(前端,前端,游戏,react,hooks,typescript)