“我正在参加掘金社区游戏创意投稿大赛个人赛,详情请看:游戏创意投稿大赛”
游戏地址: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} />
菜单配置及数据模型
菜单
<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的写法。
每个目标单词循环渲染
- 改变每帧left top direction的值
- 判断是否到了底部,到了清除循环并更新GameOver的值
- 更新目标数组及分数
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)
}
复制代码
目标单词锁定及输入框字母逐个显示匹配字母
- 先定义锁定目标单词索引
- 输入第一个字母时查找屏幕里有没有首字母与之相匹配,有将它更新并将input值渲染回输入框
- 已经有锁定单词下,判断输入的单词是否被目标单词全匹配,是更新input值,否不操作
- 当输入框的值全等于目标单词的值,将目标单词放回词库,将原先目标单词随机替换一个新的值
- 更新分数并将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('')
}
复制代码
目标单词及字母样式逐个渲染
- map渲染每个单词,当锁定单词索引与单词索引相等即添加特定样式
- 每个单词split成数组再map渲染每个字母
- 在字母索引小于输入框长度并是锁定单词的前提下,判断每个字母是否匹配输入框的值并改颜色
{/* 目标单词渲染 */}
<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和其他问题(等掘友们指出)
谁是打字王
我先来
初始速度为0.3,速度会逐渐加快,结束会有速度加分数展示
最后
制作不易、点个赞吧🙃