前言
刷视频时在某平台看到的的一款小游戏,有点想玩但又不想买,所以发动了传统艺能 —— 仿写一个。
原版示例:
复刻版:
欢迎各位来玩:
游戏介绍
问题区通过滚动的方式不停地展示题目,等滚动完一定数量的题目后,解锁答题区,开始依次填写答案,在回答完一题后问题区继续向后滚动。
是不是感觉很有意思呢😎
需求分析
首先,这是个非常小的纯前端项目,实现主要功能即可。
因为没有玩过原版的游戏,所以只能靠自己设计需求。
我设计了两种游戏模式
- 闯关模式:生成一定数量的题目,设置闯关要求的正确率。用户需要回答完所有题目,最后计算正确率是否达标。
- 无尽模式:不断地生成题目,设置最大错误量。用户可以不停地不停地答题,直到错误数量超出要求,游戏结束,最后计算用户得分。每题一分。
此外还需要要求游戏中用的参数都可自定义,所以需要做一个自定义的页面,根据需要自定义游戏难度。
项目搭建
Vite
+ Vue3
+ TS
+ Tailwind
一梭子走起。
使用 vitesse-lite 直接生成项目目录,将目录简单改造一下,替换一下 unocss
-> Tailwind
(这纯粹属于个人习惯)。
由于不需要后台交互,也没打算做的太过复杂,所以一切从简。
界面设计
对于一个小前端来说,审美问题真的是一个很大的挑战,不过好在项目并不复杂,只需要设计三个页面。
虽然要简单设计,但也不能太过潦草,选择了一些渐变色做背景,从大佬的这个网站COPY了一些动画效果。整个界面还算说得过去。
至于那些奇奇怪怪的配色则是我随便选的😜
开始开发
1. 生成题目
首先制定好生成一组题目所需要用到的参数,运算规则使用最为简单的加减乘除,其余参数则是根据不同模式的需要来制定。
/**
* 运算规则
*/
export type IMethods = '+' | '-' | '*' | '/'
/**
* 题目生成的条件
*
* type: => ’normal‘ 闯关模式, ’endless‘ 无尽模式, ’diy‘ 自定义
*/
export interface INormalOptions {
type: 'normal'
level: number
range: number // 取值范围
preNum: number // 前置题目数量
methods: IMethods[]
accuracy: number // 正确率 取百分比
questionNum: number // 题目数量
}
export interface IEndlessOptions {
type: 'endless'
errNumber: number // 错误数量
preNum: number
methods: IMethods[]
}
export interface IDiyOptions {
type: 'diy'
methods: IMethods[]
range: number
preNum: number
successType: 'normal' | 'endless' // 按照闯关模式的通关条件, 按照无尽模式的通关条件
accuracy?: number // 准确率
questionNum?: number // 题目数量
errNumber?: number // 错误数量
}
export type ICreateQuestionOptions = INormalOptions | IEndlessOptions | IDiyOptions
在游戏界面,根据路由携带的参数调用generate
来生成一定数量的题目,将题目追加到题目数组中。根据题目数量进行循环,每次都通过随机的方式选取运算规则。
/**
* 创建 num 数量的题目
* @param num
*/
const generate = (options: ICreateQuestionOptions, num: number) => {
const _questionList = []
const baseIndex = questionList.value.length
for (let index = 0; index < num; index++) {
const fn = getMethod(options.methods)
let _range = 0
/**
* 如果有预设的范围,按照预设
*/
if ('range' in options) {
_range = options.range
} else {
/**
* 没有预设范围,’endless‘模式 | 'diy'模式-’endless‘条件
*
* +、- 为20
* *、/ 为15
*/
if (fn === '+' || fn === '-') {
_range = 20
} else {
_range = 15
}
}
const question = createQuestion[fn](_range, baseIndex + index)
_questionList.push(question)
}
questionList.value.push(..._questionList)
return _questionList
}
对于不同的运算方式要做出不同的处理来保证题目的难度。
- 加法:从运算范围内随便算两个数字相加就行
- 减法:与加法同理,但要保证答案是个正数或0
- 乘法:乘除都不受运算范围的控制,因为太大的数也不好算(至少我没那个脑子去算),这里最大只取到9
- 除法:同样是为了保证题目的运算方便,显示随机取两个数进行乘法运算,将结果与其中一个数字作为问题,另一个数字作为答案。
/**
* 创建题目
*/
export const createQuestion: Record<IMethods, (range: number, i: number) => IQuestion> = {
'+': (range, i) => {
const a = getRoundNum(range - 1)
const b = getRoundNum(range - a)
const answer = a + b
return { a, b, fn: '+', answer, i }
},
'-': (range, i) => {
let a = getRoundNum(range)
let b = getRoundNum(range)
/**
* 如果a小于b,交换位置
* 保证答案为正数
*/
if (a < b) {
[a, b] = [b, a]
}
const answer = a - b
return { a, b, fn: '-', answer, i }
},
/**
* 不受运算范围的限制
* @param _
* @param i
*/
'*': (_, i) => {
const a = getRoundNum(9)
const b = getRoundNum(9)
const answer = a * b
return { a, b, fn: '*', answer, i }
},
'/': (_, i) => {
const b = getRoundNum(9)
const answer = getRoundNum(9)
const a = answer * b
return { a, b, fn: '/', answer, i }
},
}
2. 用户输入
使用系统自带的键盘来玩游戏体验十分糟糕,所以在屏幕上做了一个九宫格的小键盘,只需要 0-9 以及 删除 和 提交。
使用Grid
画个键盘还是很简单的。之后是绑定键盘事件、点击事件,这里计划做到PC端与移动端通用,所以对两端的监听事件分别做了处理。并在最后都汇总到统一的提交事件中。
/**
* 用户的答案成绩记录
*/
export const answerRecord = ref<boolean[]>([])
/**
* 当前的答案
*/
const curAnswer = ref<string[]>([])
/**
* 按钮操作
* @param key
*/
const handleCurAnswer = async(key: string) => {
/**
* 最大输入为3位数
*/
if (curAnswer.value.length > 2 && key !== 'Enter' && key !== 'Backspace')
return
if ((key === 'Enter' || key === 'Backspace') && curAnswer.value.length === 0) {
return
}
/**
* 提交当前答案
*/
const submitCurAnswer = async() => {
/**
* 获取答题结果
*/
const result = await getSubmitResult(Number(showCurAnswer.value), answerIndex.value)
answerRecord.value.push(result)
answerIndex.value += 1
curAnswer.value = []
nextTick(() => {
submitEnd(answerRecord.value)
})
}
switch (key) {
case 'Backspace':
curAnswer.value.pop()
break
case 'Enter':
submitCurAnswer()
break
default:
curAnswer.value.push(key)
break
}
|
3. 游戏结束
因为两种游戏模式的结束条件不同,所以每次提交当前答案时都要判断游戏是否结束
/**
* 判断游戏是否结束
*/
const isGameOver = (list: boolean[]) => {
if (scoreType.value === 'percentage') {
if (list.length === allQuestionLength.value) {
return true
}
} else {
if (errNumber.value > (playOptions.value as IEndlessOptions).errNumber) {
return true
}
}
return false
}
在游戏确认结束后,计算出分数与结果。
/**
* 计算得分
*/
const computedScore = (list: boolean[]) => {
const trueNum = list.filter(Boolean).length
const allNum = list.length
const options: IResultOptions = {
type: scoreType.value,
num: 0,
result: false,
}
if (scoreType.value === 'percentage') {
/**
* 闯关模式
* 计算分数,是否通过
*/
const num = Math.floor(trueNum / allNum * 100)
options.result = num >= (playOptions.value as INormalOptions).accuracy
options.num = num
} else {
/**
* 无尽模式
* 给出分数即可
*/
options.num = trueNum
}
return options
}
结语
OK,以上就是我在公司摸鱼时写的小游戏,虽然项目很小,但玩的时候也挺有意思。欢迎各位来体验呀。
最后不要脸的求个star,谢谢!