React打字版飞机大战简易实现

1,230 阅读4分钟

“我正在参加掘金社区游戏创意投稿大赛个人赛,详情请看:游戏创意投稿大赛

游戏地址:yain-spell.netlify.app
开发框架:react
运行平台:浏览器、移动端
gitee地址:yain/spell-game (gitee.com)
欢迎大家体验

前言

这次做这个的想法来自ztype,是一款很棒的单词射击游戏,没玩过的可以去体验下,用游戏引擎Impack写的,相比之下,这个就是很low的,以前是用的vue,现在新的团队只用react,所以用了react(虽然可能跟框架关系不大)。

游戏思路

1.游戏初始配置和可自定义的配置
2.实现选型 dom(改变每帧的样式) or canvas(画每帧)
3.逻辑改变样式及各种边界情况的判定

粒子背景

用react-tsparticles库,配置数据上去即可。

import Particles from "react-tsparticles";
import options from '../assets/configData'

<Particles id="tsparticles" options={options} /> 

菜单配置及数据模型

菜单

image.png

<div className='menuClass' style={{display:fix?'flex':'none'}}>
  <span onClick={()=>palyAudio()}>声音:{isPaly?'✅':'❎'}</span>
  <span>速度:
    <input type='number' min="0.1" max="5" step="0.1" 
     defaultValue={_TARGET_CONFIG.speed} onBlur={(e)=>{
     Number(e.target.value)>=0.3&&localStorage.setItem('speed',e.target.value)
     }}>
    </input>
  </span>
  <span>数量:
    <input type='number' min="1" max="6" step="1" 
    defaultValue={_MAX_TARGET} onBlur={(e)=>{   Number(e.target.value)>=1&&Number(e.target.value<=6&&localStorage.setItem('_MAX_TARGET',e.target.value)
  }}>
    </input>
  </span>
  <span>词库:
    <input type='text' defaultValue='' placeholder='请用,隔开'
    onBlur={(e)=>{localStorage.setItem('_DICTIONARY',e.target.value)}}>
    </input>
  </span>
  <span>最高纪录:{localStorage.getItem('_HIGHEST_RECORD')||0}</span>
  <span>Tips:失焦提交</span>
</div>
复制代码

数据

初始全局数据:目标量、下落速度、词库

//assest/word
//str=链接搜的词:https://blog.csdn.net/a1809032425/article/details/83961550
let wordArr = str.replace(/\W+/g,',')
wordArr = [...new Set(wordArr.split(','))]
wordArr = wordArr.filter(item=>item.length>1)
export {wordArr}

import {wordArr} from '../assets/word'
const getItem = (key) => Number(localStorage.getItem(key)) 
const _MAX_TARGET = getItem('_MAX_TARGET')||3; // 画面中一次最多出现的目标
const _TARGET_CONFIG = {speed: getItem('speed')||1,};//下落速度
const wordsPool = localStorage.getItem('_DICTIONARY')?.split(',');//自定义词库
const _DICTIONARY = wordsPool.length>1?'wordsPool':wordArr;

页面初始目标数据

  • left:为让每个单词尽量不重叠,使每个不同index在不同的区间随机生成
  • top: 为让每个单词初始时高度分开些,使每个在不同的高度区间生成,数量3即[-120,60]之间
  const wordsPool = _DICTIONARY.concat([]).sort(()=>0.5-Math.random())//乱序
  
  //text 单词  left 左边偏移量 top 上边偏移量
  let targetArr = wordsPool.splice(0,_MAX_TARGET).map((item,index)=>{
    return{
      txt:item,
      left:Math.floor((Math.random()+index)*340/_MAX_TARGET),
      top:Math.floor((Math.random() - 1)*_MAX_TARGET +1)*60,
    }
  })
  
  //direction 左右移动的方向 先有随机的left后有它
  targetArr = targetArr.map((item,index)=>({
    ...item,
    direction:item.left<175?1:-1
  }))
  
  const [state,setState] = useState({
      targetArr, // 存放当前目标
      wordsPool,//剩余单词库
      score: 0,//分数
      gameOver: false,
      color:['Blue','Pink','Aqua','FloralWhite','Chartreuse','BlueViolet']
  })
复制代码

改变每帧样式

实现用的dom(改变每帧样式),没有选择canvas主要是因为它基本上用的都是canvas相关的api去画,还要各种save,restore,用原生一样能写。dom则可以熟悉一些react的写法。

每个目标单词循环渲染

  1. 改变每帧left top direction的值
  2. 判断是否到了底部,到了清除循环并更新GameOver的值
  3. 更新目标数组及分数
  const down = ()=>{
    let newArr = [...state.targetArr]
    let timer = setInterval(()=>{
    //1
      newArr = newArr.map(item=>{
        let newDire = item.direction
        if(item.left<0||item.left>350)
        newDire = item.direction*-1
       return{
         ...item,
         top:item.top+_TARGET_CONFIG.speed,
         left:item.left+newDire,
         direction:newDire
       }
     })
     //2
     let isBottom = newArr.find(item=>item.top>=600-60) 
     if(isBottom){
       clearInterval(timer)
       setState((state)=>{
         return {
         ...state,
         gameOver:true
         }
       })
     }
     //3
     setState((state)=>{
      state.score>localStorage.getItem('_HIGHEST_RECORD')&&
      localStorage.setItem('_HIGHEST_RECORD',state.score)
       return {
        ...state,
        targetArr:newArr
        }
     })
     },17)
  }
复制代码

目标单词锁定及输入框字母逐个显示匹配字母

  1. 先定义锁定目标单词索引
  2. 输入第一个字母时查找屏幕里有没有首字母与之相匹配,有将它更新并将input值渲染回输入框
  3. 已经有锁定单词下,判断输入的单词是否被目标单词全匹配,是更新input值,否不操作
  4. 当输入框的值全等于目标单词的值,将目标单词放回词库,将原先目标单词随机替换一个新的值
  5. 更新分数并将input值和锁定目标单词索引清除(恢复默认)
//当前锁定单词在目标数组索引
  let [currentIndex,setCurrentIndex] = useState(-1)
    const inputChange = (e) => {
    if(currentIndex===-1){
      //没有目标索引 寻找输入首字母相同的单词数组并求top值大的索引 并赋给他
      let matchObj = state.targetArr.filter(item=>item.txt[0]===e.target.value&&item.top>0)
      if(matchObj.length){
        matchObj = matchObj.reduce((pre,cur)=>{
          return cur.top>pre.top?cur:pre
        })
        let tempIndex = state.targetArr.indexOf(matchObj)
        console.log(tempIndex);
        setCurrentIndex(tempIndex)
        setInput(e.target.value)
      }else{
        return
      }
    }else if(
    new RegExp(e.target.value).test(state.targetArr[currentIndex].txt)
    ){
      //单词包含输入框的单词 就setInput 当单词与输入框单词相同时就清除目标并添加下一个单词
      setInput(e.target.value)
      e.target.value===state.targetArr[currentIndex].txt &&clearAddTarget(currentIndex)
    }
  }
  
  //清除拼写完的目标放回单词库 并随机添加进一个新的
  const clearAddTarget = (index) => {
    let {targetArr,wordsPool,score} = state
    wordsPool.push(targetArr[index].txt)
    targetArr[index] = {
      txt:wordsPool[Math.floor(Math.random()*(wordsPool.length-1))],
      left:Math.floor(Math.random()*340),
      top:Math.floor(Math.random()*1),
      direction:Math.random()>0.5?1:-1
    }
    //分数加一
     score++
    setState({
      ...state,
      wordsPool,
      targetArr,
      score
    })
    //单词索引 输入框清除
    setCurrentIndex(-1)
    setInput('')
  }
复制代码

目标单词及字母样式逐个渲染

  1. map渲染每个单词,当锁定单词索引与单词索引相等即添加特定样式
  2. 每个单词split成数组再map渲染每个字母
  3. 在字母索引小于输入框长度并是锁定单词的前提下,判断每个字母是否匹配输入框的值并改颜色
{/* 目标单词渲染 */}
<div style={{width:'100%',height:'600px',position:'absolute',overflow:'hidden'}}>
       {state.targetArr.map((item,index)=>(
         <div key={index} className={`target ${currentIndex===index?'aim':''}`} style={{left:item.left+'px',top:item.top+'px',color:state.color[index]}}>
           <div style={{marginTop:'40px',minWidth:'40px',textAlign:'center'}}>{item.txt.split('').map((key,i)=>(
             <span style={{color:currentIndex===index&&key===input[i]?'yellow':''}} key={i}>
               {key}
             </span>))}
           </div>
         </div>
       ))}
 </div>
复制代码

不足

  • 整体样式差强人意
  • 没有做射击效果(每帧子弹移向锁定目标)
  • 应该还存在bug和其他问题(等掘友们指出)

谁是打字王

我先来

image.png

初始速度为0.3,速度会逐渐加快,结束会有速度加分数展示

最后

制作不易、点个赞吧🙃