【React】从零开发一个Tenzies网页小游戏

252 阅读9分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

前言

本文主要内容:通过从零开发一个Tenzies网页小游戏串联起基础的react知识点,起到复习回顾、加深印象的作用

兜兜转转看了接近一个星期才看完了scrimba上十个小时左右的课程,可以说是收获颇丰。接下来希望能多做几个项目,多看看面试八股文什么的,争取寒假的时候能找一个有效的实习做一做。

首先是第一个项目,网页上的Tenzies小游戏

项目预览与分析

image.png

给出的项目外观如图所示,主要元素有

  • 类似盒子的双色背景
  • 标题和说明规则的文字
  • 10个骰子
  • Roll按钮

游戏的规则就是点击Roll按钮,会使得10个骰子同时出现随机数,目标是达成10个骰子变成同一个数字。在这个过程中,每次Roll之间都可以选择任意数量的骰子点击冻结,这样它们就不会再被刷新。

所以接下来就让我们开始吧

1、搭建基础框架

第一步自然是要先搭建基础框架,使用npx create-react-app xxx使用临时脚手架搭建react app,然后删去暂时用不到的文件,保持程序架构的整洁,留下必要的index.htmlindex.cssindex.jsApp.js

image.png

接着清理一下这几个主要文件中的代码

首先是index.html,引入index.css和index.js,创建id为root的<div>挂载App.js

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
    <link rel="stylesheet" href="./index.css">
    <title>Tenzies</title>
  </head>
  <body>
    <script src = "../src/index.js"></script>
    <div id="root"></div>
  </body>
</html>

接着是index.js,render了App组件到index页面的root<div>上

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

最后是App.js,暂时没有内容,测试一下是否能正常生成页面

export default function App() {
  return (
    <div className="App">
     test
    </div>
  );
}

接着终端进入项目文件夹,执行npm start就能看到页面生成成功

image.png

2、创建页面背景

对于背景,我们需要在css文件中进行设置

所以进入index.css文件中,先设置颜色

body {
    background-color: #0B2434;
}

.App {
    background-color: #F5F5F5;
}

可以看到效果如下 image.png

接下来我们想要App<div>对应的内容限制在一个小方格内,所以需要设置它的大小和位置

body {
    background-color: #0B2434;
    padding: 50px;
}

.App {
    background-color: #F5F5F5;
    height: 600px;
    width: 600px;
    margin: auto;
}

最后给它加上圆角的显示,border-radius: 15px;

image.png

看到我们实现了背景的设置

3、创建十个骰子

基础样式编写

我们要做的是利用组件复用的方式编写一个组件,将每一个骰子看作一个独立的组件传入不同的props,从而获得十个样式一致,内容不同的骰子

所以在src下新建components文件夹,创建Dice.js文件,编写如下代码

import React from "react";

export default function Dice(props) {
    return (
        <div>
            <h2>{props.number}</h2>
        </div>
    )
}

所以相应的我们需要在App.js文件中使用这个组件,并且传入props

import React from "react";
import Dice from "./components/Dice";

export default function App() {
  return (
    <div className="App">
      <Dice number="1" />
      <Dice number="1" />
      <Dice number="1" />
      <Dice number="1" />
      <Dice number="1" />
      <Dice number="1" />
      <Dice number="1" />
      <Dice number="1" />
      <Dice number="1" />
      <Dice number="1" />
    </div>
  );
}

于是我们可以获得如下预览效果

image.png

接下来要做的就是样式的调整

首先调整骰子的位置,给App.js文件中的<div>下添加一个className为dice-container的<div>

因为一格一格的骰子较为规整,所以这里采用display: grid的方式,按照如下设置

.dice-container {
    display: grid;
    grid-template: auto auto / repeat(5, 1fr);
    gap: 30px;
}

接着设置骰子的样式,在Dice.js给div添加className="dice-face",然后在css文件中作以下设置

.dice-face {
    height: 75px;
    width: 75px;
    box-shadow: 0px 3px 3px rgba(0, 0, 0, 0.15);
    border-radius: 10px;
    display: flex;
    justify-content: center;
    align-items: center;
}

可以看到我们分别设置了骰子的高度宽度阴影圆角以及文字的位置

image.png

最后调整一下骰子整体的位置、字体的大小、以及骰子的背景等

在main中添加justify-content: center;align-items: center;实现居中

在dice-face中添加background-color: white;font-size: 2em;

最终可以获得如下预览效果

image.png

编写生成10个随机数数组的函数

接下来我们需要让10个骰子展示不同的随机数,所以首先得有一个生成随机数的函数如下

function newDice() {
    const newDice = []
    for (let i=0; i<10; ++i){
      newDice.push(Math.ceil(Math.random() * 6))
    }
    return newDice
}

将随机数数组转化为组件

在程序运行的时候,上面的函数会生成一个随机数数组,所以我们需要通过useState接收这个数组,并且通过map的方法转化为组件再展示到页面上

const [dice, setDice] = React.useState(newDice())使用dice state接收

const diceElements = dice.map(dice => <Dice number={dice} />)使用map转化

然后再将{diceElements}代替之前写的十个<Dice />

所以每次我们刷新页面都能获得不同的数字

image.png

4、创建Roll按钮

基础样式

dice-container下新建一个<button>,添加className="roll-dice"

所以就可以前往index.css文件添加相应的样式

.roll-dice {
    height: 65px;
    width: 135px;
    border: none;
    border-radius: 9px;
    background-color: #5035FF;
    color: white;
    font-size: 1.8em;   
}

.roll-dice:focus {
    outline: none;
}

.roll-dice:active {
    box-shadow: inset 5px 5px 10px -3px rgba(0, 0, 0, 0.7);
}

所以我们就可以获得如下的按钮

image.png

再在.App中将justify-content改为space-aroung,就能调整布局

实现Roll功能

接下来我们要为Roll按钮绑定点击事件函数,让每次点击能重新生成随机数数组

如下编写事件函数

function rollDice() {
    setDice(newDice())
}

在<button>添加onClick={rollDice}进行点击函数绑定,实现效果如下

s1x5l-nt6zt.gif

5、实现点击数字冻结

将数组转换为对象数组

游戏规则中还有一个很重要的就是可以选择投掷哪几个骰子

所以接下来我们要实现这个功能必须要确定哪几个骰子被冻结了,而这需要给将骰子由单一的数字转化为有三个属性numberidisHeld的对象

这里介绍一个小巧、安全、URL友好、唯一的 JavaScript 字符串ID生成器,nanoid。

具体安装使用方法不赘述,npm安装后,import使用即可,下面来使用它作为dice的id

首先在生成数组的函数newDice()中做出修改,将push的类型改为对象

function newDice() {
    const newDice = []
    for (let i=0; i<10; ++i){
      newDice.push({
          number: Math.ceil(Math.random() * 6),
          id: nanoid(),
          isHeld: false
        })
    }
    return newDice
}

于是就能获得如下的对象数组

image.png

再在map方法下进行修改即可

const diceElements = dice.map(dice => <Dice key={dice.id} number={dice.number}  isHeld={dice.isHeld} />)

添加数字冻结时的样式

为了一眼就能区分数字是否冻结,我们可以根据传入的isHeld修改骰子的背景颜色

在Dice.js中定义style,利用三元表达式选择背景颜色

const style = {
    backgroundColor: props.isHeld ? "#59E391" : "white"
}

再将style传到dice-face <div>中,于是将isHeld改为true,我们就能看到

image.png

实现数字点击功能

这个问题与之前写过的《【React】如何实现子组件修改父组件的state》完全一致

先创建一个修改dice state的函数,遍历每个dice,如果id一致则仅将isHeld转换,如果不一致则直接将原对象保留,代码如下

function holdDice(id){
    setDice(prevDice => prevDice.map(dice => {
      return dice.id === id ? {...dice, isHeld: !dice.isHeld} : dice
    }))
}

接着再将其化成一个新的函数作为props传入Dice组件,如下

holdDice={() => {holdDice(dice.id)}} 

我们就能实现点击换颜色的功能了

cbxuv-rsw41.gif

实现冻结功能

此时虽然我们能通过点击数字切换背景颜色,但是点击Roll按钮并未保留点击的数字

所以我们需要修改rollDice函数,让其在setDice时不只是重新生成所有的数组元素

那么就是修改setDice中的内容如下

function rollDice() {
    setDice(prevDice => prevDice.map(dice => {
      return dice.isHeld ? dice : {
        number: Math.ceil(Math.random() * 6),
        isHeld: false,
        id: nanoid()
      }
    }))
}

如果isHeld是true,那么直接保留dice,如果是false则重新生成dice对象

9gk80-s0kx1.gif

6、添加规则说明文字

没什么好特别说明的,在App的<div>里添加

 <h1 className="title">Tenzies</h1>
 <p className="instructions">Roll until all dice are the same. Click each die to freeze it at its current value between rolls.</p>

然后在css文件中添加

.title {
    font-size: 60px;
    margin: 0;
}

.instructions {
    font-family: 'Inter', sans-serif;
    font-weight: 400;
    margin-top: 0;
    text-align: center;
    font-size: 25px;
}

于是就能获得

image.png

7、判断Tenzies目标达成

现在我们已经基本完成了这个页面小游戏,不过在我们达成目标时,页面却没有给出任何反馈,这不符合一般闯关游戏的逻辑,所以接下来我们要做两件事,一是在达成目标后,Roll按钮会变成Retry;二是会有一个动画效果来庆祝完成

设置tenzies state

首先我们需要在每次重新投掷骰子后判断是否已经达成全部数字相同的目标。

而这就需要用到useEffect,它会在设定的dependency发生变化后重新render来判断是否达成目标

设定tenzies state的true和false来指代是否完成目标,如下

const [tenzies, setTenzies] = React.useState(false)

使用useEffect在每次改变dice state后都判断是否达成了目标,如下

React.useEffect(() => {
    const Number = dice[0].number
    if (dice.every(dice => dice.number === Number)) {
      setTenzies(true)
      console.log("You won!")
    }
}, [dice])

先任取了数组中的一个数,使用了数组的every方法,判断每个是否都和这个数相等

使用react-confetti实现动画效果

有了setTenzies后,就可以利用tenzies来有条件地运行庆祝动画

这里使用react-confetti这个彩带飘落的动画,所以首先使用npm安装react-confetti包

import引入后,使用如下代码

{tenzies && <Confetti />}

&& 来短路运行后面的Confetti组件,效果如下

image.png

将Roll变为Retry按钮

最后我们要做的是在达成目标后,将按钮变化并且赋予相应的功能

首先使用三元表达式改变按钮上的文字,{tenzies ? "Retry" : "Roll"}

然后修改按钮绑定的点击函数,添加一个条件语句

if(tenzies) {
  setTenzies(false)
  setDice(newDice())
} else {
  setDice(prevDice => prevDice.map(dice => {
    return dice.isHeld ? dice : {
      number: Math.ceil(Math.random() * 6),
      isHeld: false,
      id: nanoid()
    }
  }))
}

当tenzies为true时,按钮的功能为重新投掷所有骰子和将tenzeis设为false

最终效果如下

ye7y5-nh36z.gif

8、更多

基础的项目就到这里结束了,不过过两天可能会给这小页面再添加一些小改动,包括但不限于

  • 用CSS构建骰子上的点取代数字
  • 实时显示点击次数
  • 记录完成时间
  • 保存上述两项数据构建排行榜
  • ...

9、小结

花了六个多小时从头到尾总结回顾了react的相关内容,写了这个简单的小项目,做一个简单的小结,来描述一下写项目的心路历程

  1. 首先是使用create-react-app创建项目,以及一系列的初始化,已经很熟练了
  2. 然后是CSS样式的编写,分别使用了grid和flex两种布局
  3. 写了生成随机数数组的函数,用到了js中的相关知识,push Math
  4. 接着就将数组转化为了组件,使用了map的方法
  5. 然后为了能单独地对每个数字进行操作,将数字改成了有三个属性的对象
  6. 通过将id传入的方式实现了子组件对父组件的操作
  7. 最后用了useEffect实时监控dice的变化

虽然是个很简单的项目,不过确实是用到了之前学到的所有react基础知识,很有收获

原项目地址:scrimba.com/learn/learn…

我的github地址:github.com/newfish-cmy…