场景

2018/08/21 Mmo-Game

场景的设计在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> cacheChildren = new TIntObjectHashMap<>();

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);
    }

}

销毁数据,例如场景模块的数据在这里进行销毁

移除,标记回归。就是从运行的子组缓存中移除。

Search

    Table of Contents