一种手游中实时战斗系统的设计思路

﹥>﹥吖頭↗ 提交于 2020-01-07 16:42:22

【推荐】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();

}


易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!