场景的设计在mmo游戏里是比较核心的内容,这部分是我老大写的,写的很不错,值得我们学习下。
目录
场景一些基本概念
在我们的游戏里,场景是继承AbstractService类,这里的AbstractService是业务上使用的服务,服务会被线程池调用。
可以简单理解为每个AbstractService是单独的一个线程,线程在线程池里执行,每个场景由单独的线程去驱动。所以,我们
在业务上如果将一些变量放置到了场景上,需要特别注意不同场景下多线程可能引起的问题。
场景组和单一场景
一:场景组
1.适用场景
组场景适合人数较少,tick执行时间较短的场景类型,经验值是小于10人的场景,比如:单人副本、组队副本等等。
人数多了场景tick时间会上去,那么按顺序执行场景组就会有较大延迟。
2.场景组意义
AbstractSceneGroupService中存储了一个场景数组,当适用于组场景的场景启动后会将场景add到AbstractSceneGroupService中的场景数组,
每个组场景中存放多少个场景是根据公式:100 / 单场景容纳人数来进行计算的。
组场景的意义就在于减少了ProcessorPool中的service数量,直接的减少了线程的调度量,对CPU的利用率更高。
3.场景组执行
AbstractSceneGroupService由于是继承于AbstractService的,所以它可以在ProcessoPool中被线程调度执行,
AbstractSceneGroupService在执行tick时会按照数组顺序依次执行场景的tick。
/**
* 场景组service,管理一组场景的tick
* 这一组场景tick是按照先后顺序进行tick的
* 这里并没有处理每个个体的frame rate,processor在调用tick的时候
* 是根据SceneGroup的frame rate调用的
* Created by wangqiang on 2017/9/21.
*/
public abstract class AbstractSceneGroupService extends AbstractService
{
/**
* 存在该场景组下的所有场景数组
*/
private ArrayList<AbstractSceneService> sceneServiceArray = new ArrayList<>();
/**
* 待加入的场景service队列
*/
private LocklessList<AbstractSceneService> waitingAddService = new LocklessList<>();
/**
* 待删除的场景service队列
*/
private LocklessList<AbstractSceneService> waitingRemoveService = new LocklessList<>();
@Override
public void tick(int interval)
{
while (true)
{
AbstractSceneService service = waitingAddService.pop();
if (service != null)
{
service.active();
sceneServiceArray.add(service);
}
else
{
break;
}
}
int size = sceneServiceArray.size();
if (size < 1)
{
return;
}
long start = System.currentTimeMillis();
for (int i = 0; i < size; i++)
{
long end = System.currentTimeMillis();
AbstractSceneService sceneService = sceneServiceArray.get(i);
// tick时间需要处理一下,因为每个tick都是阻塞的,所以interval时间需要每次计算
sceneService.tick0(interval + (int)(end - start));
}
while (true)
{
AbstractSceneService service = waitingRemoveService.pop();
if (service != null)
{
service.inactive();
sceneServiceArray.remove(service);
}
else
{
break;
}
}
}
代码省略
...
...
...
}
分析:单人副本,组队副本…适用于场景数组的场景会加入到waitingAddService,这样可以减少线程池中线程的数量。tick执行的时候,我们可以看到组场景确实是会按照数组顺序依次执行场景的tick。
二:单一场景
1.适用场景
单一场景适合人数较多、tick执行时间较长的场景类型,比如:主城、野外。
2.单一场景执行
当场景启动后将场景直接add到ProcessorPool中进行调度,在ProcessorPool中独占一条线程执行一次tick,tick完成后释放占用的线程,然后等待下一次继续独占线程执行tick。
场景和线程
场景不与执行线程绑定,场景的每一个tick是由哪条线程执行的需要在ProcessorPool中进行调度,不绑定线程的目的是为了做场景与线程的负载均衡。
场景集体初始化过程
一:场景的创建在服务器启动时就全部创建好,采用使用时再派发线程执行的方式
注意:现在我们游戏设计是服务器启动时就全部创建好场景,然后如果线数量不够那么去激活新的线,但是这么设计会有问题,如果表配置开线过多会导致内存撑爆。还有可能有些场景人去的少,那么场景线开启过多,造成内存浪费。个人认为,如果是设计线人数满后再创建新场景,激活新的线,这么设计会好一些。但是可能会引起新的问题,场景初始化效率会过慢,那么是否可以考虑加个平滑过渡?
private boolean loadScene(DictSceneDefineData sceneDefineData)
{
...
...
for (int i = 0; i < maxLineCount; i++)
{
int lineID = i + 1;
AbstractBPScene sceneService = CreateSceneFactory.createScene(sceneDefineData);
if (sceneService == null)
{
return false;
}
//这里会一下子new出最大分线个数的场景单元!!
SceneUnit sceneUnit = new SceneUnit();
}
...
...
sceneService.init();
sceneService.initFirst();
...
...
}
分析:关键代码,这里for循环里一下创建了最大线数maxLineCount个场景单元SceneUnit,然后调用init初始化场景。
注意!这里的初始化只能初始化数据,因为现在只是单场景每次在分配新线加入到线程池时,调用了下inactive。
场景回收,是不做数据销毁工作的,只是将service从池中移除而已。数据还是保存在内存中。
二:场景tick
/**
* 激活一个新的场景线路,激活,并执行
* @param container
* @return 激活的线id
*/
private int activeNewSceneline(SceneContainer container)
{
...
...
// 单一派发
if (container.getSceneStructType() == SceneStructTypeEnum.SINGLE)
{
runNewSceneLine(sceneUnit);
}
else
{
SceneUnitGroup group = container.allocateSceneGroup(sceneUnit);
if (group == null)
{
return BPErrorCodeEnum.SCENE_LINE_IS_FULL;
}
runNewSceneLine(sceneUnit, group);
}
...
...
}
/**
* 将指定的scene unit激活执行,scene unit中的
* service派发给processor池执行
* 该方法只适合一个线对应一个service的情况
* service组的相关派发使用另外一个同名方法
* @param sceneUnit
*/
private void runNewSceneLine(SceneUnit sceneUnit)
{
//这里看出每个场景其实就是一个线程service,不良人项目的场景基类AbstractBPScene extends AbstractSceneService
AbstractSceneService service = sceneUnit.getSceneService();
runningScene.add(sceneUnit);
sceneUnit.setActive(true);
//增加一个线程任务到线程池
CoreGlobals.getInstance().getProcessorPool().addService(service, sceneUnit.getFrameRate());
}
/**
* 将指定的scene unit放在service group中激活执行
* 该方法将多个service组合成一个组,由一个processor按顺序执行
* @param sceneUnit
* @param sceneUnitGroup
*/
private void runNewSceneLine(SceneUnit sceneUnit, SceneUnitGroup sceneUnitGroup)
{
AbstractSceneService service = sceneUnit.getSceneService();
//场景组service----AbstractSceneGroupService
//只将一个场景service加入到线程池
AbstractSceneGroupService groupService = sceneUnitGroup.getGroupService();
if (!sceneUnitGroup.isRunning())
{
CoreGlobals.getInstance().getProcessorPool().addService(groupService, sceneUnit.getFrameRate());
sceneUnitGroup.setRunning(true);
}
//后面再来的service不加入到线程池而是加入到场景组AbstractSceneGroupService队列里按顺序执行。
runningScene.add(sceneUnit);
sceneUnit.setActive(true);
groupService.addSceneService(service);
}
分析:将AbstractSceneService和AbstractSceneGroupService服务加入线程中执行。
/**
* 单场景service
*/
public class Processor implements Callable<AbstractService>
{
...
...
@Override
public AbstractService call() throws Exception
{
try
{
if (!isActive)
{
service.inactive();
service.active();
isActive = true;
}
service.tick0(interval);
}
catch (Exception e)
{
e.printStackTrace();
CoreLog.CORE_COMMON.error("processor call exception:{}", ExceptionUtils.exceptionToString(e));
}
finally
{
statusEnum = ProcessorStatusEnum.IDLE;
return service;
}
}
...
...
}
分析:单场景AbstractSceneService线程池执行,调用call()方法执行对应service场景tick。
特别注意单场景类型每次将场景线程加入到线程池时去tick值行前,会先进行一个inactive()清除销毁!!!
我们需要在inactive()方法里进行一些相关状态的销毁!!!
/**
* 场景组service,管理一组场景的tick
* 这一组场景tick是按照先后顺序进行tick的
* 这里并没有处理每个个体的frame rate,processor在调用tick的时候
* 是根据SceneGroup的frame rate调用的
* Created by wangqiang on 2017/9/21.
*/
public abstract class AbstractSceneGroupService extends AbstractService
{
@Override
public void tick(int interval)
{
while (true)
{
AbstractSceneService service = waitingAddService.pop();
if (service != null)
{
//激活时没有调用inactive清除!!!
service.active();
sceneServiceArray.add(service);
}
else
{
break;
}
}
int size = sceneServiceArray.size();
if (size < 1)
{
return;
}
long start = System.currentTimeMillis();
for (int i = 0; i < size; i++)
{
long end = System.currentTimeMillis();
AbstractSceneService sceneService = sceneServiceArray.get(i);
// tick时间需要处理一下,因为每个tick都是阻塞的,所以interval时间需要每次计算
sceneService.tick0(interval + (int)(end - start));
}
...
...
}
}
场景组是一个service去tick场景组AbstractSceneGroupService。
tick场景组时:
激活场景组队列里的场景service没有调用inactive清除!!!
tick时间需要处理一下,因为每个tick都是阻塞的,所以interval时间需要每次计算。
三:场景销毁
if (actorHashMap.isEmpty())
{
if (!DictSceneDefineData.isGuildScene(sceneID) && !isSpaceScene() &&
actor.getExceptionEnum() != ObjectExceptionEnum.NONE)
{
// 通知world,需要销毁场景
BPSceneEmptyNotice notice = new BPSceneEmptyNotice(MessageIDConst.SW_SCENE_EMPTY_NOTICE);
notice.setSceneID(sceneID);
notice.setLineID(lineID);
sendServiceMessageToWorld(notice);
}
}
场景玩家为空,不是帮派,位面,没有异常,通知世界回收场景。
/**
* 回收场景单元
* @param sceneUnit
*/
public void recycleSceneUnit(SceneUnit sceneUnit)
{
int lineID = sceneUnit.getSceneLine();
AbstractBPScene scene = sceneUnit.getSceneService();
if (sceneStructType == SceneStructTypeEnum.SINGLE)
{
//单场景,其实就只是将service从线程池中移除而已,不会tick了,没有做任务的销毁工作。
//所以此时该场景所有的缓存数据到只是暂存到内存中而已
CoreGlobals.getInstance().getProcessorPool().removeService(scene);
}
}
单场景回收,其实就只是将service从线程池中移除而已,不会tick了,没有做任务的销毁工作。
所以此时该场景所有的缓存数据到只是暂存到内存中而已!!!
那么再次利用该场景service时一定要做清零工作,不然报错!!
所以,单场景激活加入到线程池时,会调用service.inactive(),先清除下数据,注意重新激活时不会调用场景init()方法!!!
**
* 回收场景单元
* @param sceneUnit
*/
public void recycleSceneUnit(SceneUnit sceneUnit)
{
else
{
int groupIndex = sceneUnitGroupMap.get(lineID);
SceneUnitGroup group = null;
if(groupIndex >= 0 && groupIndex<sceneArray.size())
{
group = sceneGroupArray.get(groupIndex);
}
if (group != null)
{
group.decrementService();
group.removeSceneService(scene);
}
}
}
/**
* 场景组service,管理一组场景的tick
* 这一组场景tick是按照先后顺序进行tick的
* 这里并没有处理每个个体的frame rate,processor在调用tick的时候
* 是根据SceneGroup的frame rate调用的
* Created by wangqiang on 2017/9/21.
*/
public abstract class AbstractSceneGroupService extends AbstractService
{
@Override
public void tick(int interval)
{
//将场景service从场景组队列里删除,不进行tick
while (true)
{
AbstractSceneService service = waitingRemoveService.pop();
if (service != null)
{
//回收时调用了inactive清除数据
service.inactive();
sceneServiceArray.remove(service);
}
else
{
break;
}
}
}
}
场景组回收是从场景组队列里移除了而已,回收时调用了inactive清除数据! 但是,场景组激活时是没有调用inactive清除的!!!
四:总结
abstractBPScene.init();
abstractBPScene.initFirst();
if (i < minLineCount)
{
//激活一个新的场景线路 加入到线程池tick
activeNewSceneline(sceneContainer);
}
项目一定要特别注意,是先调用场景的init()方法,然后再激活service,并且init()只是在起服时调用一次。
然后单场景或场景组在执行激活时都会调用active()激活方法。
单场景在激活service时会调用inactive()方法做数据销毁,但是回收时没有做处理,只是将数据都暂存内存里。
多场景在加入到场景组队列时是没有调用inactive()方法做数据销毁,但回收场景时有调用inactive()方法做数据清除。
!!!!!需要特别注意单场景和场景组初始化init()方法和active()激活方法使用:
单场景:
在起服时都会调用init()方法初始化,我们可以在init()初始化方法里先做:new数组、new Map,但是不能装数据,因为激活时还会先调用inactive()清除数据。
然后调用active()方法,一般不在这个激活方法做处理。
单场景回收只是将service从线程池里移除,没有做任何销毁数据工作,所以数据还是常驻在内存,直到从新在激活调用inactive(),调用active()。
不要在init()方法里做数据赋值。如果做了赋值,那么就不能在inactive里进行销毁工作。否则,先init()赋值,然后激活时调用了inactive()又把数据销毁了。
场景组:
宝箱,一开始是关闭状态,如果玩家打开后变成永久打开状态。
那么我们不能在inti()方法里初始化将宝箱关闭状态加入到场景里。因为init()方法只是在起服时调用一次。场景组回收,调用inactive()方法,
将宝箱从场景里做删除了,那么下次激活场景组时,没有初始化宝箱了,场景里没放上宝箱。
所以,我们把宝箱初始化添加到场景放到active()方法,每次重新激活都会重新初始化宝箱到场景里!!!
位面
一:位面场景不是一上来先初始化好
private boolean loadScene(DictSceneDefineData sceneDefineData)
{
//这里如果是位面并启用,那么直接return返回,也就是位面不是一开始就上来初始化好的
if(sceneDefineData.getIsSpace() && GameConstant.useSpace)
{
return true;
}
....
....
}
这里如果是位面并启用,那么直接return返回,也就是位面不是一开始就上来初始化好的
二:位面场景表数据处理,SceneDefine表
/**
* 位面场景绑定组 key为主城场景id,value类位面场景id列表 (注意都是场景id)
*/
private static TIntObjectHashMap<TIntArrayList> spaceSceneDic;
if(config.getIsSpace())
{
TIntArrayList list=spaceSceneDic.get(config.bindTownID);
if(list==null)
{
list=new TIntArrayList();
spaceSceneDic.put(config.bindTownID,list);
}
//绑定场景ID
list.add(config.getId());
}
这里的意思是,每个主城会绑定多个位面。例如:308场景,多个位面都可以共用这个308场景。
三:场景位面模块的初始化
/**
* 场景位面模块
* @version 创建时间:2018/3/12
*/
public class SceneSpaceModule extends AbstractSceneModule
场景位面模块是怪AbstractBPScene场景基类下,所以场景基类init的时候,也会初始化场景位面模块。
@Override
public void init()
{
//判断当前主城id是否绑定位面组
TIntArrayList list = DictSceneDefineData.getSpaceSceneList(getScene().getSceneID());
if (list != null)
{
for (int i = list.size() - 1; i >= 0; --i)
{
//位面场景id
int sceneID = list.get(i);
DictSceneDefineData config = DictSceneDefineData.getRecordT(sceneID);
ArrayList<AbstractBPScene> tList = new ArrayList<>();
//缓存的子组
cacheChildren.put(sceneID, tList);
//位面最小值
for (int j = config.getMinSpaceCount() - 1; j >= 0; --j)
{
//创建位面场景,其实也是副本场景,不过会加个子场景标识
AbstractBPScene scene = CreateSceneFactory.createScene(config);
//将当前场景标识为子场景,再tick的场景时会单独处理
scene.getSpaceModule().setChildScene(true);
//设置父类场景,也就是绑定的主场景
scene.getSpaceModule().setParent(getScene());
scene.setSceneID(sceneID);
scene.setDictSceneDefine(config);
scene.init();
scene.initFirst();
tList.add(scene);
}
}
}
}
主场景下如果绑定了位面那么初始化创建位面场景,其实也是副本场景,但是会进行额为的标识:
先加入到缓存的子场景组
private TIntObjectHashMap<ArrayList
1.创建位面场景,其实也是副本场景
2.将当前场景标识为子场景,再tick的场景时会单独处理(这个额外的标识非常重要)
3.设置父类场景,也就是绑定的主场景
4.位面场景初始化
四:位面的场景的tick
位面是单人进入的,不存在多人同场景释放技能…等较大的性能销毁,所以位面场景的tick是做了单独额外处理的。
AbstractBPScene.java场景基类下:
@Override
public void tick(int interval)
{
//如果不是子场景,所以就是主场景都会调用tickChildren
if (!spaceModule.isChildScene())
{
spaceModule.tickChildren(interval);
}
}
public void tickChildren(int interval)
{
//运行子组不为空(主场场景绑定的位面有正在运行)
if (!children.isEmpty())
{
TIntObjectIterator<AbstractBPScene> it = children.iterator();
AbstractBPScene scene;
while (it.hasNext())
{
it.advance();
scene = it.value();
//需要tick消息
scene.tick0(interval);
if (scene.getSpaceModule().removed)
{
int sceneID = scene.getSceneID();
scene.inactive();
//移除
it.remove();
//标记回归
scene.getSpaceModule().removed = false;
//入缓存
ArrayList<AbstractBPScene> list = cacheChildren.get(sceneID);
if (list == null) {
list = new ArrayList<>();
cacheChildren.put(sceneID, list);
}
list.add(scene);
runningChildrenDic.adjustValue(sceneID, -1);
}
}
}
}
场景基类下,如果不是子场景,都会调用tickChildren。
运行子组不为空(主场场景绑定的位面有正在运行),则会进行tick处理。
从这里可以看出,位面场景其实也是for循环迭代里去tick的,
位面是单人进入的,不存在多人同场景释放技能…等较大的性能销毁,所以位面场景的tick是做了单独额外处理的。
五:任务进位面,位面场景激活
现在机制是,任务接取了,如果当前是个位面任务,那么,前端会直接再发起进入副本的协议。
位面也副本一种类型。所以走副本通用申请进入。
/** 申请进入副本 */
public boolean applyEnter(int id)
/** 执行进入副本 */
private void doEnterCopyScene(DictCopySceneData config,int lineID,boolean needTeam)
{
...
...
if(sceneDefineConfig.getIsSpace() && !needTeam && GameConstant.useSpace)
{
if(actor.getScene().getSpaceModule().isChildScene())
{
BPLog.BP_LOGIC.error("不能在位面中再进位面");
return;
}
if(nextSpaceScene != null)
{
BPLog.BP_LOGIC.error("已经申请位面");
return;
}
int curSceneID = actor.getScene().getSceneID();
if (sceneDefineConfig.getBindTownID() != curSceneID)
{
BPLog.BP_LOGIC.error("位面绑定的主城id与当前场景不一致");
return;
}
//创建子场景
AbstractBPScene childScene= actor.getScene().getSpaceModule().createChildScene(sceneID);
...
...
进位面场景
}
...
...
}
不能在位面中再进位面
已经申请位面
位面绑定的主城id与当前场景不一致
创建子场景
进位面场景
/** 创建子场景(活动的) */
public AbstractBPScene createChildScene(int sceneID)
{
ArrayList<AbstractBPScene> list = cacheChildren.get(sceneID);
if (list == null)
{
list = new ArrayList<>();
cacheChildren.put(sceneID, list);
}
DictSceneDefineData config = (DictSceneDefineData) DictSceneDefineData.getRecordById(sceneID);
if (null == config)
{
return null;
}
if (config.getMaxSpaceCount() > 0)
{
//已经满了
if (runningChildrenDic.get(sceneID) >= config.getMaxSpaceCount())
{
return null;
}
}
AbstractBPScene scene;
if (list.isEmpty())
{
scene = CreateSceneFactory.createScene(config);
scene.getSpaceModule().setChildScene(true);
scene.getSpaceModule().setParent(getScene());
scene.setSceneID(sceneID);
scene.setDictSceneDefine(config);
scene.init();
scene.initFirst();
}
else
{
scene = list.remove(list.size() - 1);
}
scene.getSpaceModule().setChildInstanceID(getInstanceIDAdder());
//位面场景激活
scene.active();
//加入到运行子组
children.put(scene.getSpaceModule().getChildInstanceID(), scene);
runningChildrenDic.adjustOrPutValue(scene.getSceneID(), 1, 1);
return scene;
}
这里最重要的就是:
位面场景激活,这里例如刷怪,场景事件都是在激活的时候做的。
加入到运行子组
六:任务完成或失败退出位面,位面进行销毁
位面退出会调用exitCopyScene()方法
/**
* 退出副本
* @param actor
* @param isForce 是否是强制退出副本,即跳过相关判断
*/
public void exitCopyScene(Actor actor, boolean isForce)
{
...
...
if(isSpace && GameConstant.useSpace)
{
actor.getCopySceneModule().doExitSpace();
re = 0;
}
...
...
}
退出位面也就是走正常切换场景,只是再切场景前做一些位面额外处理,例如设置坐标什么的…
// 从场景中移除场景对象
private void doRemoveBpObjectFromScene(BPObject bpObject)
{
...
...
if (actorHashMap.isEmpty())
{
sceneActorIsEmpty();
Actor actor = (Actor) bpObject;
if (!DictSceneDefineData.isGuildScene(sceneID) && !isSpaceScene() &&
actor.getExceptionEnum() != ObjectExceptionEnum.NONE)
{
// 通知world,需要销毁场景
BPSceneEmptyNotice notice = new BPSceneEmptyNotice(MessageIDConst.SW_SCENE_EMPTY_NOTICE);
notice.setSceneID(sceneID);
notice.setLineID(lineID);
sendServiceMessageToWorld(notice);
}
}
...
...
}
正常的切场景,从场景中移除场景对象,如果当前场景没有玩家了,场景会进行回收。
但是位面没玩家了,不是在这里进行回收。
/**
* 删除切换场景状态中的actor
* @param actorID
*/
public void removeSwitchingActor(long actorID)
{
Actor actor = switchingSceneActorMap.remove(actorID);
if (actor != null)
{
if (spaceModule.isChildScene())
{
//位面可删除标识
spaceModule.setRemoved(true);
spaceModule.getParent().spaceModule.removeSpaceActor(actor);
}
}
}
删除切换场景状态中的actor,如果是位面场景,那么将当前位面场景标为可删除。
public void tickChildren(int interval)
{
...
...
//如果位面可以删除
if(scene.getSpaceModule().removed)
{
int sceneID=scene.getSceneID();
//销毁数据
scene.inactive();
//移除
it.remove();
//标记回归
scene.getSpaceModule().removed=false;
//入缓存
ArrayList<AbstractBPScene> list=cacheChildren.get(sceneID);
if(list==null)
{
list=new ArrayList<>();
cacheChildren.put(sceneID,list);
}
list.add(scene);
runningChildrenDic.adjustValue(sceneID,-1);
}
}
销毁数据,例如场景模块的数据在这里进行销毁
移除,标记回归。就是从运行的子组缓存中移除。