代练服务功能设计
需求背景
项目会提供一些游戏相关的服务。
这其中,就包含有代练这一项服务。简单来说,就是玩家雇佣我们去代替玩家本人进行游戏。
通常是在一些需要长时间投入的游戏中,玩家会选择代练。
需求分析
代练的种类有很多,比如:
- 完成游戏内的任务。
- 通关副本。
- 打败 Boss。
- 推进游戏剧情。
- 提升等级。
而每个游戏的情况也都不一样。
有些游戏具有设备平台的限制,所以我们需要一个设备平台的模块。
而有些游戏虽然有设备平台的区分,但是它允许设备互通数据,取而代之的是不同的服务器之间不允许互通数据。
有些游戏有周目系统,一个周目内的 Boss 都只能打一次。如果希望再打一次,就需要通关游戏进入下一周目。游戏难度也随着周目的增加而增加。
而有些游戏只要玩家有足够的入场券,就可以一直打。
而这大多数游戏都存在升级这一系统,玩家可以通过升级来提升自己的能力,使得可以打败更强的 Boss。
通过以上分析,我们可以得出需要以下模块:
- 游戏平台和游戏服务器,我们可以使用单选框的形式。因为它们都是固定的,不可能说代练前半段去玩 PC 平台的 A 服务器,后半段去玩 Xbox 平台的 B 服务器。
- 游戏周目,我们可以采用下拉框的形式,尽管游戏难度会随着周目提升而提升,但总会有上限,因此它将是一个固定的列表。
- Boss 列表,我们可以采用复选框的形式,通常存在 Boss 的游戏,都将不止一位 Boss,而且 Boss 是可以并列的,比如打败 A,B,C 三位 Boss。
- 打 Boss 的次数,我们可以采用滑块的形式,因为它是一个连续的数值,可以随时调整。
- 角色升级,我们可以采用双向滑块的形式,因为代练的角色的等级是不一样的,用户可能游玩了一段时间,也提升了一些角色等级,因此我们需要让用户自由调整角色当前等级和希望达到的等级。
设计方案
我们需要的是一个通用的代练系统。
这个代练系统要求:
- 可变性,不局限于单一游戏。
- 可扩展,方便随时增加新模块。
- 可维护,方便随时修改。
- 可复用,方便随时使用。
基于以上要求,我们将明确以下几点:
- 不能使用固定的模块,比如选择游戏平台,游戏服务器。
- 因为它们强依赖于游戏。
- 同时会导致在增加新代练时,不管是否具有游戏平台,游戏服务器,都必须增加对应字段。
- 我们不可能每增加一个代练,都增加一个新的字段。
- 可以灵活的增加新的功能。
- 增加的功能不会影响到现有数据。
- 也就是说,我们不会去为旧数据增加关于新增功能的字段。
- 每个产品都是独立的,不会因为增加功能而需要修改。
- 即使修改了已有功能,也不会影响到其他功能。
- 比如我们修改了单选框,也不会影响到下拉框。
- 功能应该是复用的。
- 比如 Boss 列表,我们可以在多个代练中使用,而不是每个代练都增加一个 Boss 列表的功能。
- 我们根据数据变化展示,而不是变化功能。
而通过对于以上的分析,我们已经得到了需要的模块:
- 单选框。
- 下拉框。
- 复选框。
- 单向滑块。
- 双向滑块。
设计实现
我们将根据模块化思想,采用组件的形式,实现一个可以复用功能,且易于扩展的代练系统。
定义数据结构
我们需要定义一个数据结构,来存储代练的信息。
这个数据结构需要包含以下内容:
- 代练产品有哪些选项。
- 比如游戏平台,游戏服务器。
- 每个选项都有什么子选项。
- 比如游戏平台的 PC, PS,Xbox。
- 每个子项的内容。
- 展示的文本,比如 “服务器XX”。
- 选项的价格。
- 选项的代练时间。
- 选项的默认信息,比如是否默认选中。
- 此外,还有选项的规则,比如滑块的最大最小值等。
- 代练的描述。
- 代练名称。
- 代练价格。
- 代练耗时。
- 代练图片。
根据以上要求,我们可以确定数据结构如下:
首先它应该是一个未知长度的数组,因为我们不知道代练产品它都会有哪些选项。
数组元素应该是一个对象,含有选项标题,选项类型和子选项列表。
子选项应该含有以上列出信息。
于是我们可以得到以下数据结构:
/**
* 选项类型
*/
export const optionsType = [
'button', // 按钮组类型,对应的是单选框,因为在项目中,代练的单选框是以按钮的形式展示的。
'checkbox', // 复选框类型
// "radio",
'select', // 下拉框类型
'range', // 单向滑块类型
'double-range', // 双向滑块类型
] as const satisfies string[]
/**
* 选项数据
*/
type ObjectItem = {
/**
* 选项id
*/
id: number
/**
* 选项标题
*/
title: string
/**
* 子选项列表
*/
options: Array<{
/**
* 子选项id
*/
id: number
/**
* 子选项文本
*/
value: string
/**
* 子选项价格
*/
price: number
/**
* 子选项是否选中
*/
checked: boolean
/**
* 子选项时间
*/
time: number
/**
* 子选项是否隐藏
*/
hide: boolean
/**
* 子选项规则
*/
rule: string
}>
type: optionsType[number]
}
/**
* 代练数据
*/
type BoostingData = {
/**
* 产品名称
*/
productName: string
/**
* 基本价格
*/
baseTime: number
/**
* 基本时间
*/
basePrice: number
/**
* 选项
*/
data: ObjectItem[]
/**
* 描述
*/
description: string
}
实现选项组件
依次实现各个选项类型对应的组件,注意在组件中涉及选项变化的统一交由父级处理,简单来说就是在父组件传入一个事件到选项组件中,选项组件触发了比如选中,滑动之类的事件就调用它。
除此之外,组件不会有其他的逻辑。
因为组件的实现比较简单,这里就不赘述了。
// 组件 ...
{
options.map((item, index) => {
if (item.type === 'button') {
return (
<ButtonGroup
key={index}
data={item}
changeOption={changeOption}
/>
)
}
if (item.type === 'select') {
return (
<SelectGroup
key={index}
data={item}
changeOption={changeOption}
/>
)
}
// ...
})
}
// 组件 ...
以上是我们的渲染逻辑,可以看到,逻辑非常简单,就是根据数据类型,渲染对应的组件。
但是这样如果后续增加了新的功能,我们就需要增加新的判断。
由于我们传入的 props
都是统一的。
我们可以将这段逻辑简化。
// 提前打表,将组件和类型对应起来
const BoostingComponents = {
button: ButtonGroup,
select: SelectGroup,
//...
}
// 组件...
{
boostingData.map((item, index) => {
// 根据选项类型,从选项表中获取对应的组件
// 需要做一些边界判断,比如选项类型不存在等
// 这里省略了
const Component = BoostingComponents[item.type]
return (
<Component
key={index}
data={item}
changeOption={changeOption}
/>
)
})
}
实现代练功能
以下是关于代练功能的函数签名。
/**
* 改变选项
* @param father 父级选项id
* @param child 子级选项id
* @param field 字段
* @param value 值
*/
function changeOption(father: number, child: number, field: string, value: any): void {}
/**
* 计算价格和时间
* @param options 选项列表
* @returns 价格和时间
*/
function cumputePriceAndPrice(options: ObjectItem[]): { price: number; time: number } {}
选项之间的联动
虽然我们完成了选项组件,但是我们还没有实现选项之间的联动。
比如,当我们选择了一个游戏平台,我们就需要显示对应的游戏服务器。
我们引入一个新的数据结构,来存储选项之间的联动关系。
type ReactiveItem = {
/**
* 响应id
*/
id: number
/**
* 选项id
*/
optionId: number
/**
* 子选项id,多个 id 会以逗号分隔
*/
objectId: string
/**
* 子选项数据字段
*/
field: string
/**
* 子选项修改后的值
*/
value: number
/**
* 是否为覆盖数据
*/
merge: boolean
/**
* 是否为倍率
*/
rate: boolean
}
type BoostingData = {
/**
* 产品名称
*/
productName: string
/**
* 基本价格
*/
baseTime: number
/**
* 基本时间
*/
basePrice: number
/**
* 选项
*/
data: ObjectItem[]
/**
* 选项修改后影响其他选项
*/
reactive: ReactiveItem[]
/**
* 描述
*/
description: string
}
以下是处理选项联动的相关函数签名。
/**
* 应用联动到选项
* @param father 选项索引 比如平台,服务器的索引
* @param child 子选项索引 比如平台的 PC,PS 的索引
* @param field 子选项的数据字段
* @param value 修改的子选项的数据
* @param options 代练选项数据
* @returns 应用变化后的代练选项数据
*/
function applyReactiveToOptions(father: number, child: number, field: string, value: number, options: ObjectItem[]): ObjectItem[]
type ReactiveRecord = Map<number, {field: 'price' | 'checked' | 'time' | 'hide', merge: boolean, value: number, rate: boolean}[]>
/**
* 加载响应内容
* @param reactives 响应内容
* @param onlyRate 是否仅倍率记录
* @returns 响应记录
*/
function loadReactive(reactives: ReactiveItem[], onlyRate = false): Map<number, ReactiveRecord>
当调用 loadReactive
方法时,它会返回一个键为子选项 ID, 值为响应记录的 Map 数据结构。
而响应记录则是一个键为需要修改的子选项 ID,值为对应响应变化的 Map。
即,首先根据变化的选项查询它是否具有需要关联修改的子选项,如果存在,则执行关联修改。
页面加载时,我们首先创建 reactives
和 rates
这两个变量存储普通的联动信息和倍率联动信息。具体的话就是一个是 +10, 一个是 +10%。
applyReactiveToOptions
的工作流程是这样的。
- 首先
options[father].options[child][field] = value
将对应的子选项数据修改为新值。 - 接着,查询
options[father].options[child].id
是否在reactives
中存在关联选项信息,如果有就进行修改。 - 接着,查询
options[father].options[child].id
是否在rates
中存在关联选项信息。 - 最后,返回修改后的选项数据。
之所以要进行两次查询,是因为倍率联动和普通联动是不同的。
倍率联动是因为倍率联动是根据最后的总价来进行计算的。比如 +10%。
以上,就是我们的代练功能的设计。