第一个React项目——单机版宝石商人小游戏~

最近开始学习React了,为了学习过程更有趣味性,选择了一个简单有趣的桌游宝石商人来开发,边玩边开发边学习!用的是最基础的技术栈 create-react-app + react-redux + react-router-dom + antd,桌游界面的设计主要是参考【哇哦桌游】的,不过做的是单机版嘛,所以有一些改进。主要是想分享一下好玩的桌游和自己的设计,记录一些开发中遇到的问题,大概包括这些:

  1. 游戏流程 & 界面组件设计
  2. 数据设计 react-redux
  3. react中数据状态存储的方法和区别
  4. 为什么在setInterval中获取不到最新的数据
  5. 设置key={index}的问题

目前就先想到这么多,突然记起来啥再加上来吧~


1. 游戏流程 & 界面组件设计

宝石商人这个游戏大致流程就是2个玩家(其实可以多个但是我只做了2个)轮流换取宝石或者是卡牌,谁先凑齐15分谁就赢了。

  • 获取分数有2个渠道,一是兑换有分数的卡牌(在卡牌的右上角标明的是分数),二是获取贵族的青睐(满足贵族所要求的卡牌数量即可获得青睐)。
  • 每个回合可以选择3种操作,(1)拿取宝石,只能拿白色、蓝色、绿色、红色和褐色宝石,不同颜色的至多3个,相同颜色的至多2个;(2)预约卡牌,预约后这个卡牌不能被另一个玩家抢走,且会附赠一个黄色的宝石(可以当做任何颜色使用),下个回个就可以选择兑换预约的卡牌了,一共只有5个黄色宝石,所以2个玩家最多一共预约5个卡牌;(3)兑换卡牌,满足相应卡牌的颜色即可完成兑换。
  • 兑换得到的卡牌有颜色,这个颜色的卡牌相当于永久的同颜色的宝石。
  • 用户选择的内容会实时显示,当用户确认选择后,才会提交至用户信息中进行保存。

根据上述游戏流程,就可以进行界面和组件的设计啦~

整个界面为一个父组件Game,包括User组件用于表示用户,显示当前用户得分、卡牌数量、宝石数量、预约的卡牌和倒计时,Card组件用于表示中间可选择的卡牌,Noble组件显示贵族青睐所需的卡牌,Gem组件用于显示当前剩余的宝石数量,Round组件用于显示当前回合用户选择的内容。

image.png

2. 数据设计 react-redux

  • Q1:关于为什么使用redux

其实一开始我是考虑直接把所有数据都放在父组件Game上,再通过props传递给子组件数据和修改数据的方法,这样子组件可以拿到所有的数据并可以修改数据,但是最主要的问题是,这样数据不方便进行规范化的修改和管理,子组件修改的数据很容易出错。所以最后还是选用react-redux进行了数据管理,在reducer中定义了修改每个数据的方法,这样都可以对数据进行规范化的修改,开发中遇到的问题会更少。不知道这样的理解是否正确诶

  • Q2:是一个action和reducer管理一个数据吗?

使用react-redux的时候,在教程中,是着重强调了给每个组件定义一个reducer文件,但是我觉得应该是给每个数据定义一个reducer文件,文件内记录对于每个数据的操作方法,如果是每个组件一个reducer的话,那两个组件都要用一个数据怎么办?我觉得好像是教程有问题,我是按照每个数据一个reducer进行开发的!

一、系统数据:
系统数据是游戏初始化的数据,随机从以下数据中选择,需要选择3个贵族以及4张一级卡牌、4张二级卡牌和4张三级卡牌,选择的贵族和卡牌之间不能重复。

  1. nobles 贵族的青睐,表示需要什么要求的卡牌才能够获得贵族的青睐,+3分
[
    {
        blue: 4,
        white: 4,
    },
    // ……
]    
复制代码
  1. 一级、二级、三级卡牌,一级卡牌20张,二级卡牌30张,三级卡牌40张。
{
    level3: [
        {
            score: 5,
            color: 'blue',
            needs: {
                    white: 7,
                    blue: 3,
            },
        },
        // ……
   ],
   level2: [
        {
            score: 3,
            color: 'blue',
            needs: {
                    blue: 6,
            },
        },
        // ……
   ],
   level1: [
        {
            score: 1,
            color: 'blue',
            needs: {
                    red: 4,
            },
        },
        // ……
   ]
}
复制代码

二、redux管理的数据一共包括:

  1. remainGem:存储剩余宝石的数量
 { white: 5, blue: 5, green: 5, red: 5, brown: 5, gold: 5 }
复制代码
  1. round:存储当前是第几个回合,以及当前的用户
{
    number: 1, // 回合数
    userIndex: 0, // 用户id
}
复制代码
  1. userList:存储当前的用户信息
[{
    name: '玩家1',
    score: 0, // 得分
    gem: { white: 0, blue: 0, green: 0, red: 0, brown: 0, gold: 0 }, //宝石
    card: { white: 0, blue: 0, green: 0, red: 0, brown: 0 }, // 卡牌
    order: [], // 预定的卡牌
},
{
    name: '玩家2',
    score: 0,
    gem: { white: 0, blue: 0, green: 0, red: 0, brown: 0, gold: 0 },
    card: { white: 0, blue: 0, green: 0, red: 0, brown: 0 },
    order: [],
}]
复制代码
  1. selectedContent:当前回合用户选择的内容,分为4种类型。

(1)用户选择的为 宝石

{
    type: 'gem',
    content: {
        gemList: ['white', 'blue', 'yellow'],
    },
};
复制代码

(2)用户选择的为 卡牌

{
    type: 'card',
    content: {
        card: {
            score: 3,
            needs: { red: 3, blue: 3,  green: 3, white: 3 }
        }
    },
};
复制代码

(3)用户选择的为 预定

{
    type: 'order',
    content: {
        card: {
            score: 3,
            needs: { red: 3, blue: 3,  green: 3, white: 3 }
        },
        gemList: ['gold'] // 预定默认获得一个金色的万能宝石
    },
};
复制代码

(3)用户选择的为 该用户之前预定的卡牌

{
    type: 'order-card',
    content: {
        card: {
            score: 3,
            needs: { red: 3, blue: 3,  green: 3, white: 3 }
        },
    },
};
复制代码

修改每种数据的方法如下:

数据 方法 含义
remainGem changeRemainGem 修改剩余的宝石数量,type选择+1/-1,gemList选择需要操作的宝石是哪些
round changeRound 修改当前回合为下一个回合
userList addUser 初始化增加一个玩家
changeUserSelectedGem 修改玩家已有的宝石数量
changeUserSelectedCard 修改玩家已有的卡牌数量
changeUserScore 修改玩家得分
changeUserOrderList 修改玩家的预定内容
changeUserWin 改变玩家的状态为赢得游戏(可能某个玩家赢了,也可能平局)
selectedContent addSelectedGem 增加当前用户选择的宝石
reduceSelectedGem 减少当前用户选择的宝石
changeSelectedCard 修改当前用户的选择内容为某卡牌
changeSelectedOrder 修改当前用户的选择内容为预定某卡牌
changeSelectedOrderCard 修改当前用户的选择内容为该用户已经预定的某卡牌

其中,之所以需要设计selectedContent这个数据,是因为,用户选择的内容是显示在Round组件内的,并且当前用户在结束回合前,都可以修改选择的内容,只有选择结束回合后,才会将选择的内容提交至User组件显示。

3. react中数据状态存储的方法和区别

在一个类组件中,数据状态有如下的方式进行存储:

  1. 在this实例上
  2. 在this.state中
  3. static静态属性
class Components extends React.Component {
    this.test = 1; // 方式 1:在this实例上
    constructor (props){
        super(props);
        this.state = {
            test: 1 // 方式 2:在this.state中
        }
    }
}
Components.test = 1; // 方式 3:静态属性
复制代码

这3种方式的区别在于:

  1. 在this.state中存储的是和视图相关的响应式数据,当state中相关的数据变化后,需要通过setState更新数据,从而使得react帮助我们更新视图。
  2. 在this实例上存储的数据,该组件的每个实例都存在test属性,且每个实例的test属性之间互不相干,一般存储实例需要的、和视图不直接相关的数据。
  3. 静态属性存储的是和实例无关的数据,是每个组件实例共享的数据,通过【组件名.属性名】访问。

同样的,在一个函数组件中,数据状态存在如下4种类型存储:

  1. useState
  2. useRef
  3. 静态属性
  4. var/let/const一个变量
function Component(){
    const [test, setTest] = useState(1); // 方式 1
    const testRef = useRef(1) // 方式 2
    var test1 = 1; // 方式 3
    // ……
    return <div></div>;
}
Component.test = 1; // 方式 3:静态属性 
复制代码

这4种方式的区别在于:

  1. 方式1是响应式数据,是和视图直接相关的数据,需要通过setTest方式更改数据。
  2. 方式2不是响应式的,通过testRef.current = 2的方式更改数据。
  3. 方式3是静态属性,是通过Component.test访问的,各组件示例之间可以共享。
  4. 方式4中的test1变量是一次性的,每次视图更新调用Component函数获取最新的render内的时候,都会重新定义一个test1变量,并赋值为1,所以test1只能参与一些一次性的运算。

4. 为什么在setInterval中获取不到最新的数据

之所以会关注3的数据存储方式,是因为我在开发过程中遇到了这样一个问题:

  • 场景:我采用的函数组件进行开发,由于需要用户操作的倒计时,我在User组件中定义了一个定时器,每0.1秒都会改变用户的剩余时间,从而使得进度条发生变化。
  • 数据:我是使用的useRef方法存储的定时器,因为定时器和响应式视图不直接相关,但是视图更新后是需要获取到之前的定时器的,这样使得定时器不会重复定义、方便清除定时器,而和视图相关的剩余时间我是使用useState定义的,因为它与视图直接相关。
function User(props){
    const [selectRemainTime, setSelectRemainTime] = useState(120); // 剩余时间
    const taskRef = useRef(null); // 定时器
    if (task.current === null && props.index === props.round.userIndex) {
        // 当前没有定义定时器,且当前用户是本组件实例的时候
        taskRef.current = setInterval(() => {
            setSelectRemainTime(v => v - 1); // 更新时间
            // 有一些条件需要清除定时器
            // !!!
            // 这里的selectRemainTime一直是初始值
            if ( selectRemainTime < 0 ) {
                clearInterval(taskRef.current);
                // !!!
                // 这里需要从props上获取react-redux更新的数据,做出一些操作
                // 但是获取不到最新的prosp
            }
        })
    }
}
复制代码

为什么在setInterval内获取不到最新的selectRemainTime和props数据呢?这主要是闭包导致的,当前函数内没有selectRemainTime和props,所以只能去父作用域内找,找到的selectRemainTime和props是初始化时候的值,不是更新后的值。

所以解决的办法包括:

  1. 使用useEffect,在useEffect的依赖数据数据中增加selectRemainTime。
  2. 使用useRef,增加对于selectRemainTime和props的引用,每次都使用useRef更新当前的值。
  3. 使用官方提供的useCallback。

注意,使用useEffect解决props的变化是没有用的,因为props的属性值并未变化,变化的是深层次的对象,所以react-redux相关的数据的变化useEffect的监听不到的。

最后,我是采用了这样的方式解决selectRemainTime和props数据更新的问题:

function User(props){
    const [selectRemainTime, setSelectRemainTime] = useState(120); // 剩余时间
    const taskRef = useRef(null); // 定时器
    const propsRef = useRef(props); // props的引用
    propsRef.current = props; // 更新引用
    if (task.current === null && props.index === props.round.userIndex) {
        // 当前没有定义定时器,且当前用户是本组件实例的时候
        let count = selectRemainTime; // 形成一个闭包
        taskRef.current = setInterval(() => {
            setSelectRemainTime(--count); // 更新时间和count变量
            if ( count < 0 ) {
                clearInterval(taskRef.current);
                // propsRef.current上获取最新的数据
            }
        })
    }
}
复制代码

selectRemainTime采用了闭包解决,props采用了useRef解决。

5. 设置key={index}的问题

很多数据都是采用的Object类型来存储,在jsx语法的时候都需要写成

Object.keys(data).map(e => (<div></div>))
复制代码

的方式,但是一般控制台会出现缺少key的warning,在我开发过程中倒是因为缺少key没有出现什么错误,但是为了一个干干净净的控制台~ 还是解决一下这个问题吧!

这个其实和react的渲染有关(和vue的原理是一样的),就是在通过diff算法比较新的和旧的虚拟dom树的时候,会根据key来判断,这个dom元素是否需要更新,在vue中如果没有设置,默认为undefined,而在react中,没有设置默认为index。这都会使得渲染的效率下降,例如插入新元素的时候,如果不合理的设置key,会导致多更新一个元素。

如果数据只需要渲染,而不存在更新,则设置key={index}是没有问题的,因为不需要更新不存在diff;而如果涉及到更新,但是没有input等输入型组件的增删改,设置key={index}也是没有问题的,但是可能会使得渲染效率下降;如果涉及到input输入组件的增删改,则使用key={index}很有可能会出现问题。

可以作为key的数据:

  1. 数据的唯一值,例如id值等等;
  2. 在数据初始化的时候,增加一个id值,可以使用时间戳、随机数等等。

最近领了个腾讯云免费的服务器,部署了一下,好像4月十几号就到期啦,地址是:
http://42.192.152.124:5277/splendor/
,不知道现在还有没有bug,我改了好几版了 T T

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享