之前在群里突发奇想了很多内容,希望对所有开发者都有帮助。为大家提供一下我个人的开发思路,做一个封存
装备套装激活思路
很多人开发游戏时候,武器装备都是一旦激活就立刻将值写死,这对于后期维护是不合理的。一般建议是 基础数值 + 浮动数值 = 实际数值 的思路。
意思是获得装备时,实际上获得的是 模板数值 生成的 编号面板,这个面板只是模板数值的百分比。当需要调出这个对应编号的装备时,会通过 模板 去 按照编号面板 取得 计算实际的基础装备白板
这样方便后期 削弱/增强 对应武器,并立即生效到所有玩家装备上
代码内容如下:
const weapon = {
"尻川皮带": {
lastId: 11,
atk: 33
}
}
const weaponMap = {
// 武器模板数据
weapon,
// 武器品质模板
qualityMap: {
'下等': 0.8,
'中等': 1,
'上等': 1.2
},
// 初始化构建新的武器数据浮动面板
cerateWeaponInfo(weaponName) {
if (!this.weapon[weaponName]) return -1
// 获取最新武器的 id
const id = ++this.weapon[weaponName].lastId
// 获取武器的品质的参考值
// 这里应该为浮动的区间,在这里设置 倍率 或者 浮动区间
const qualityValue = Math.random() * 1.1 + 0.4
// 获取对应的武器品质评价
const dict = Object.keys(this.qualityMap)
const qualityName = dict.find((item, index) => {
return qualityValue >= this.qualityMap[item] && dict[index + 1] ? qualityValue < this.qualityMap[dict[index + 1]] : true
})
// 生成武器浮动数据
const map = {
weaponName,
qualityValue,
qualityName,
value: 1,
upValue: 0,
lv: 1,
id
}
console.log(map.qualityName, map.qualityValue, `武器伤害值:${Math.floor(map.qualityValue * weaponMap.weapon[weaponName].atk)}`);
return map
},
// 获取实际武器面板属性
getWeaponInfo(id, weaponName) {
// 略...
}
}
// 获得 100 次尻川皮带
for (let i = 0; i < 100; i++) {
// 获得装备的对象信息
const result = weaponMap.cerateWeaponInfo('尻川皮带')
}
效果截图
当需要调整 尻川皮带面板的攻击时候,只要将模板数值调整即可。
会立即生效到所有玩家装备。
因此,玩家在需要的情况下,后台应立刻获取最新的武器面板参数更新。
套装加成思路
当装备了不同的武器,如果实现了套装效果,应立即增益对应的套装加成。多个套装会多次加成。
// 假设当前持有装备
const take = ['麻痹戒指', '屠龙宝刀', '暗黑护甲', '暗黑头套']
// 套装内容和效果
// have 表示需要装备的内容
// up 表示加成的内容
// {key} 代表套装的名字
const upSet = {
'自由,深邃黑暗': {
have: ['暗黑护甲', '暗黑头套'],
up: {
stk: 22,
def: 10
}
},
'麻酥酥': {
have: ['麻痹戒指'],
up: {
stk: 11,
def: 5
}
},
'屠龙勇士': {
have: ['屠龙宝刀', '屠龙王冠'],
up: {
stk: 11,
def: 5,
hp: 14
}
},
'光之勇士-改': {
have: ['屠龙宝刀', '暗黑护甲'],
up: {
stk: 33,
def: -10,
hp: 200
}
}
}
// 获取用户装备是满足在套装加成
const result = checkUpBuff(take, (obj) => {
const dict = { stk: '攻击力', def: '防御力', hp: '血量' }
console.log(`你目前激活了${obj.name},${Object.keys(obj.up).map(item => {
return `${dict[item]}:${obj.up[item]}`
}).join('、')}`);
})
console.log(result);
// 判断持有套装和总加成
function checkUpBuff(take, fn) {
// 初始化总加成
const upInfo = {}
// 初始化套装内容
const upSetName = []
// 遍历套装数据
Object.keys(upSet).forEach(item => {
// 判断是否符合需求
const isUse = upSet[item].have.every(up => {
return take.includes(up)
})
// 如果符合 叠加对应加成
if (isUse) {
const upKey = Object.keys(upSet[item].up)
upSetName.push(item)
// 回调函数 用于每次触发响应 (达成成就的判断?)
fn && fn({ name: item, up: upSet[item].up })
upKey.forEach(up => {
if (!upInfo[up]) upInfo[up] = 0
upInfo[up] += upSet[item].up[up]
})
}
})
// 返回加成和加成套装名对象
return { upSetName, upInfo }
}
效果截图
立刻获得加成信息,可以在每次必要场景时额外赋值
本地存储思路
由于大型游戏少不了存储本地数据的方法,实际上每次行动或者获得道具时候直接发起本地读写是比较耗时的。
很多游戏的存储方法是定时、存档点后存储,这种优化可以用于游戏开发中。
设置定时存档,初始化时直接提取所需的本地数据转为临时数据。并周期性将临时同步至本地数据中。以下是某个作者按这思路制作的 openword 的部分代码
async function getStoreWordMap() {
const result = await getOrCreateFile(path.join(ctx.baseDir, config.historyData), true);
return JSON.parse(result);
}
async function setStoreWordMap(map) {
const mapStrData = JSON.stringify(map);
await setOrCreateFile(path.join(ctx.baseDir, config.historyData), mapStrData);
return true
}
async function getStoreUserArea() {
const result = await getOrCreateFile(path.join(ctx.baseDir, config.playerArea));
return JSON.parse(result);
}
async function setStoreUserArea(userLive) {
const userLiveStrData = JSON.stringify(userLive);
await setOrCreateFile(path.join(ctx.baseDir, config.playerArea), userLiveStrData);
return true
}
async function getStoreUserInfo() {
const result = await getOrCreateFile(path.join(ctx.baseDir, config.userInfo));
return JSON.parse(result)
}
async function setStoreUserInfo(userInfo) {
const userLiveStrData = JSON.stringify(userInfo);
await setOrCreateFile(path.join(ctx.baseDir, config.userInfo), userLiveStrData);
return true
}
const word = new WorldMap(config.wordSize)
// 本地存储方案
const StoreFn = {
getStoreWordMap,
setStoreWordMap,
getStoreUserArea,
setStoreUserArea,
getStoreUserInfo,
setStoreUserInfo
}
// 本地化存储
async function setStoreFn() {
// 批处理
await Promise.all([
StoreFn.setStoreWordMap(word.map),
StoreFn.setStoreUserInfo(userInfo),
StoreFn.setStoreUserArea(word.userLive)
])
console.log('完成本地存储');
}
ctx.setInterval(async () => {
await setStoreFn()
}, 600000)
效果截图
没有实际代码,提供一个相关日志吧。
词库的开关功能,是在每次启动时完成所有分群数据的载入。并周期性判断是否有更新的数据,如果有,抽出更新的数据,依次存储对应更新的数据至本地存储中。
业务区分思路
有人开发游戏,会在程序处理时,返回一段或多段文本。然后拿着这个文本以消息的方式发送,这个其实是不太合理的。
如果只是小游戏,这倒也没什么。真正应该 逻辑层 和 显示层 分离。方便后期维护和更新界面层样式。
逻辑层只做逻辑层的事情,返回一段信息参数。提供给界面层渲染样式,提供消息接口发送。
不仅游戏应该这样,其他业务也可以这样。方便更新样式或者更新逻辑。
同步更新/多人协作 都互不干扰;
下面提供某个开发者做的 photodisk 插件的部分代码
async addAuthority(userInfo) {
if (this.userInfo[userInfo.userId]) {
return {
msg: '权限用户已注册,无需再次注册!',
result: false
}
}
const userName = userInfo.userName ? userInfo.userName : `manager_${userInfo.userId.substring(userInfo.userId.length - 4)}`;
this.userInfo[userInfo.userId] = { userName, userId: userInfo.userId, power: 2 }
await this.updateStoreAuthorityData();
return {
msg: `注册权限成功,名字为 ${userName}`,
result: true
}
},
async delAuthority(userId) {
if (!this.userInfo[userId]) {
return {
msg: '不存在该权限用户,删除失败',
result: false
}
}
if (this.userInfo[userId].power >= 3) {
return {
msg: `无法通过指令去删除最高管理员 ${this.userInfo[userId].userName} 的权限\n请在后台删除`,
result: false
}
}
const delName = this.userInfo[userId].userName;
delete this.userInfo[userId];
await this.updateStoreAuthorityData();
return {
msg: ` 删除 ${delName} 权限用户成功`,
result: true
}
},
async changeAuthorName(userId, userName) {
this.userInfo[userId].userName = userName;
await this.updateStoreAuthorityData();
return {
msg: `改名成功!你的权限账号的新别名为 ${this.userInfo[userId].userName}`,
result: true
}
可以看到他写了类似后端的代码,返回了个处理状态 code,和消息提示参数。
说明他其实已经在为后期界面翻新做准备,方便后面更新显示层,因为显示层明确知道传过来的是什么,只要思考对应样式展示即可。