【推荐】2019 Java 开发者跳槽指南.pdf(吐血整理) >>>
引言
现在的手游玩法越来越复杂,特别是战斗系统,再也不是以前那种简单的回合制模式。越来越多的手游采用了实时战斗的模式(如刀塔传奇),玩法有点类似于以前的即时战略游戏,这对于程序设计提出了更高的要求。本文提出了一种手游中实时战斗系统可行的设计思路。
设计需求
实时战斗,不同于早期页游和手游单纯的看战报或回合制模式,整个战斗过程是流畅和连贯的,人物的移动、攻击、技能释放都不会让玩家感觉到停滞,整体感觉类似于传统的即时战略游戏(魔兽、星际等),玩家在游戏中的指令(如释放技能)可以实时得到执行。
这里带来的问题是,如何设计一个稳定且高效的战斗系统,来满足多人战斗时可能的高并发;不会因为高并发对服务器造成过重的负担,不会对玩家带来糟糕的延时体验;同时数目繁多的兵种和技能要能够稳定有序地工作在这个系统中,不会让程序员疲于应付而无所适从。
下面针对这些需求提出了一种设计思路。
设计要点:
内存化
单线程
分解
当玩家在线人数很多时,如果还是将每次数据修改入库,势必会带来很大的cpu开销。笔者曾经参与一个项目,当同时在线人数达到500时,服务器用于mysql的cpu占用率飙到了800%。后来经过分析,有很大一部分数据没必要实时入库,例如战场上的NPC数据,相对不敏感,即使服务器重启也无所谓,这部分数据可以全走内存;另有一部分玩家相关数据可以采用异步存储的方式,战斗线程直接操作内存,另有一监控线程视情况每隔一段时间将内存数据刷入数据库。
也许你会说,现在的多核服务器为什么还要用单线程?这是因为单线程有它的好处,一是不用费心费力去解决死锁等并发问题,通常一个先后关系造成的死锁问题会占用程序员大量的解决时间;二是有了前面的内存化,战斗线程不再会因为数据库读写等耗时操作而卡帧,所以我们完全可以用这样一个模型来解决问题:只有一个后台线程在逐帧循环,每帧的战斗数据推送前端;玩家的操作(如释放技能)不直接执行,而是交由后台线程排队后逐个执行。执行的时刻可能是当前帧,或者推迟到下一帧,总之由后台线程统筹规划。这样就避免了因为并发带来的一些未知问题。
我们来比较一下两种程序设计思路:一是把所有的战斗逻辑都写在后台线程里,一大堆if-else和for循环耦合在一起;二是将复杂问题分解到很多类中,每个类只负责处理它应该处理的事情,后台线程做的事情只是按一定的顺序把这些类组织起来。可以明显看到第二种方法更加清爽,代码可维护性更好,程序员也更喜欢。事实上,很多战斗系统都同样可以分解为battleUnit, state, skill, buff等基本的单元。下面会专门举例说明。
举例:
下面这个例子假定战斗发生在一个战场(FightScene)中,战场中有许多战斗单位(FightUnit),有一个战斗引擎(FightEngine)负责开启后台线程,每帧遍历一次战场中的各个战斗单位,进行相应的动作。玩家释放技能的操作,由事件(Event)的方式通知对应的战斗单位,更改它的状态机使之进入技能状态(SkillState)并执行释放技能和添加buff(Buff)的操作。整个系统只有一个线程,战斗过程模块化,结构清晰,易于扩展。
// buff
public interface Buff {
// 进入时调用
public void enter();
// 退出时调用
public void exit();
// 每帧执行
public void tick(long interval);
}
// 事件
public interface Event {
}
import java.util.List;
// 战场
public class FightScene {
// 攻方列表
private List<FightUnit> attList;
// 守方列表
private List<FightUnit> defList;
// 后台线程每隔一帧执行一次
public void tick(long interval) {
for (FightUnit unit : attList)
unit.tick(interval);
for (FightUnit unit : defList)
unit.tick(interval);
}
// 寻找指定战斗单位
public FightUnit findUnit(int side, int index) {
if (side == 1) {
return attList.get(index);
}
else {
return defList.get(index);
}
}
}
import java.util.List;
// 战斗单位(玩家或者npc)
public class FightUnit {
// 事件列表
private List<Event> eventList;
// buff列表
private List<Buff> buffList;
// 当前状态(状态机)
private State state;
// 添加事件
public void addEvent(Event event) {
eventList.add(event);
}
// 添加buff
public void addBuff(Buff buff) {
buffList.add(buff);
}
// 每帧执行
public void tick(long interval) {
for (Event event : eventList) {
if (event instanceof SkillEvent){
int skillId = ((SkillEvent)event).getSkillId();
// 退出旧的状态,进入新的状态
state.exit();
state = new SkillState(this, skillId);
state.enter();
}
}
for (Buff buff : buffList) {
buff.tick(interval);
}
state.tick(interval);
}
}
// 战斗引擎
public class FightEngine {
private FightScene fightScene;
// 帧间隔
public static final long TICK_INTERVAL = 50;
// 启动战斗线程
public void startFightThread() {
new Thread(){
public void run() {
while (true) {
try {
long startTime = System.currentTimeMillis();
fightScene.tick(TICK_INTERVAL);
long endTime = System.currentTimeMillis();
// 补足一帧剩余时间
Thread.sleep(TICK_INTERVAL - (endTime - startTime));
} catch (Exception e) {
// TODO
}
}
}
}.start();
}
// 释放技能(这里是玩家操作)
public void releaseSkill(int skillId, int side, int index) {
/*
* 判定条件(能量不足、战斗已结束等)
* TODO
* ...
*
* */
// 添加事件到战斗单元
FightUnit unit = fightScene.findUnit(side, index);
unit.addEvent(new SkillEvent(skillId));
}
}
// 技能事件(一种事件的类型)
public class SkillEvent implements Event{
// 技能id
private int skillId;
public SkillEvent(int skillId) {
this.skillId = skillId;
}
public int getSkillId(){
return skillId;
}
}
// 技能状态(一种状态类型)
public class SkillState implements State{
// 技能id
private int skillId;
// 战斗单位
private FightUnit unit;
public SkillState(FightUnit unit, int skillId) {
this.unit = unit;
this.skillId = skillId;
}
// 进入时调用
public void enter() {
}
// 离开时调用
public void exit() {
}
// 每帧执行
public void tick(long interval) {
/*
* 释放技能的逻辑(一连串令人眼花缭乱的效果...)
* TODO
* ...
*
* */
// 添加buff(假定这个技能会给自己加buff)
Buff buff = /*...*/
unit.addBuff(buff);
buff.enter();
}
}
// 战斗单位的状态
public interface State {
// 每帧执行
public void tick(long interval);
// 进入时调用
public void enter();
// 离开时调用
public void exit();
}
来源:oschina
链接:https://my.oschina.net/u/614879/blog/550227