组队

2018/09/06 Mmo-Game

mmo游戏里组队核心设计和思考(只包含核心部分)

目录

队伍创建

一:场景创建队伍信息发送世界

BPSWCreateTeam msg = new BPSWCreateTeam();
//创建队伍成员数据(这里是队长,玩家简版一些数据、外显)
msg.setMemberData(createTeamMemberData());

...
...

//new一个新的目标数据发送到世界
msg.setTarget(lastTarget.clone());

sendMessageToWorld(msg);

玩家创建队伍如果设置了目标(等级要求,目标id…),客户端会上发目标信息。

没有设置目标,默认给一个(1-120级)。

创建队伍成员数据(这里是队长,玩家简版一些数据、外显)。

new一个新的目标数据发送到世界,防止引用直接修改。

二:场景创建队伍信息发送世界处理

...
...
//删除成员匹配
removeMemberMatch(memberData.getActorID());

//世界队伍数据
WorldTeamData team = new WorldTeamData();
team.setTarget(target);//直接赋值, 因为是clone过来的

//队伍序号
team.setId(++teamIndex);
team.setLeaderID(memberData.getActorID());
team.memberIndexPlus(1);
memberData.setIndex(team.getMemberIndex());

//加入成员组
team.putMemberData(memberData.getActorID(),memberData);

//加入成员队伍反查字典
actorTeamDic.put(memberData.getActorID(),team.getId());

//加入到队伍组,目标队伍字典
addTeam(team);

//给自己发送进入
BPWSEnterTeam msg = new BPWSEnterTeam(memberData.getActorID());
//克隆一份数据
msg.setTeamData(team.copy());

sendActorServerMessage(msg);

删除成员匹配,构建世界队伍数据,

加入一些缓存的字典组。

克隆一份队伍数据,发送场景,给自己发送创建队伍。

三:世界发送场景给玩家发送创建队伍结果

...
...
// 进队伍前推送一键清除
oneKeyCleanList(TeamClearListType.INVITE, false);

//先设置队伍数据
teamData = data;

//缓存数据到场景
//队员切换场景后托管状态下,如果队长在坐骑上,队员也上坐骑(可能失败)
doEnterScene();


//刷aoi队伍数据(不包括自己)
AbstractBPScene scene = getActor().getScene();
if (null != scene)
{
    scene.getTeamModule().refreshActorTeamInfo(getActor());
}

//自己单独走消息处理

//如果不对就再再刷一次

//如果当前出于不可队伍操作的状态

//  不能跟随, 解除跟随
if(!isLeader() && canStartFollow() < 0)
{
    //  不能跟随, 解除跟随
    toChangeFollow(false, false);
}

这里会缓存队伍数据到场景,并同时判断跟随时队长是否在坐骑上,队长如果在坐骑上,队员也会尝试上坐骑。

考虑下队员如果上坐骑失败,速度会和队长不一致?有进行额外处理?

不是队长,canStartFollow()会判断能否跟随。

玩家申请队伍

这里我们其实可以明白,为什么队伍数据需要存世界,而不能存场景。

玩家申请队伍可以是不同场景操作的,所以队伍数据需要存世界,然后场景发送世界进行验证。

一:场景发送申请玩家数据到世界

...
...
//是否已经有队伍

//是否可以组队操作

// 加个申请队伍的cd
if(applyTeamCDTicker.containsKey(teamID))

//发送世界
BPSWApplyTeam msg = new BPSWApplyTeam();
msg.setTeamID(teamID);
//new一个申请者数据
msg.setMemberData(createTeamMemberData());
sendMessageToWorld(msg);

主要操作加申请cd,

new一个包含申请者玩家数据,

发送申请的队伍i到世界进行验证。

二:世界验证玩家队伍申请

//根据申请的队伍id拿取缓存在世界上的队伍数据
WorldTeamData team = getTeam(teamID);

//队伍是否已满
if(team.isFull())

//重复申请
if(team.getApplySet().containsKey(memberData.getActorID()))

//加入到申请列表

//移除申请列表中最早的

{
    // 告诉队长, 有人申请加入
    BPWSApplyTeamToLeader msg = new BPWSApplyTeamToLeader(team.getLeaderID());
    // 申请者数据
    msg.setMemberData(memberData);
    msg.setApplyTime(getNowTickTime());

    sendActorServerMessage(msg);
}

//告诉玩家申请队伍成功
{
    BPWSApplyTeamSuccess msg = new BPWSApplyTeamSuccess(memberData.getActorID());
    msg.setTeamID(teamID);

    sendActorServerMessage(msg);
}

世界上做一些校验,成功后,给场景里的队长和申请者返回申请成功信息。

三:队长接收到入队申请

...
...
//已经没了队伍
if(!hasTeam())

//已不是队长了
if(!isLeader())
...
...
// 存一下(申请列表, 用于断线重连之后的发送, 由world的协议添加或者删除)
applyInfos.put(memberData.getActorID(), pbObj);
// 申请时间, scene不处理tick. 只用于显示
applyTimes.put(memberData.getActorID(), new ApplyTimeInfo(memberData.getActorID(), applyTime));

//是否自动同意申请
if(actorTeamConfig.isAutoAcceptApply())
{
    agreeApplyTeam(memberData.getActorID());
}
else
{
    //不是自动申请模式发送玩家,感觉这里不需要重新new一个申请者数据
    BPTeam.SCSendApplyTeam.Builder builder = BPTeam.SCSendApplyTeam.newBuilder();
    builder.setData(memberData.createPBObj());
    getActor().sendPacket(PacketIDConst.SCSendApplyTeam,builder.build());

    //推送红点
    getActor().getRedPointModule().addRedPointEvent(RedPointTypeEnum.APPLY_TEAM);
}

判断是否有队伍,是不是队长,存申请、申请时间。

分为自动同意申请和手动同意申请。

1.自动同意申请

public void agreeApplyTeam(long actorID)
{
    BPSWAgreeApplyTeam msg = new BPSWAgreeApplyTeam();
    msg.setTeamID(teamData.getId());
    msg.setActorID(actorID);
    
    sendMessageToWorld(msg);
}
发送到世界操作
**
* 同意申请加入队伍
* @param teamID 队伍id
* @param applyActorID 申请者id
*/
public void agreeApplyTeam(int teamID,long applyActorID)
{
    ...
    ...
    //该队员已不在申请组中
	if(!team.getApplySet().containsKey(applyActorID))

    //目标不存在
	if(!getWorldService().getSceneManager().getPlayerExist(applyActorID))

    // 玩家不存在或者不在线
    WorldActorData worldActorData = getWorldService().getActorDataByID(applyActorID);
    if (worldActorData == null || !worldActorData.isOnline())

    //删除一个申请列表
	team.deleteApplyInfo(this, applyActorID);

    //发送到申请者(世界下发场景玩家,场景玩家必须在线)
	BPWSAgreeApplyTeamToMember msg = new BPWSAgreeApplyTeamToMember(applyActorID);
    ...
    ...

}

主要判断,是否在申请组中,目标是否存在,玩家是否在线

发送到申请者(世界下发场景玩家,场景玩家必须在线)

/**
* 同意申请入队消息到队员
* 队员此时还需要做一些判断,因为世界下发到场景有延时,队员可能已经进入其他队伍...
* @param teamID
* @param leaderID
*/
public void onAgreeApplyTeamToMember(int teamID,long leaderID)
{
    //已经有队伍了
	if(hasTeam())

    //当前不可操作
	if(!canOperateTeam())

    ..

    BPSWSendAgreedApplyTeamMemberData msg = new BPSWSendAgreedApplyTeamMemberData();
    msg.setTeamID(teamID);
    //创建队伍成员数据发送世界
    msg.setMemberData(createTeamMemberData());
}

队员此时还需要做一些判断,因为世界下发到场景有延时,队员可能已经进入其他队伍…

再发送世界处理。

/**
* 添加成员到世界队伍中
* @param team
* @param memberData
*/
private void addMemberToTeam(WorldTeamData team,TeamMemberData memberData)
{
    ...
    ...
    //移除个人匹配
	removeMemberMatch(memberData.getActorID());

    //成员序加1
    team.memberIndexPlus(1);
    memberData.setIndex(team.getMemberIndex());

    ...
    ...

    //先广播给队员有人加入队伍了
    TLongObjectIterator<TeamMemberData> it = team.getIterator();
    while(it.hasNext())
    {
        it.advance();
        
        //推送回
        BPWSAddTeamMember msg = new BPWSAddTeamMember(it.value().getActorID());
        msg.setMemberData(memberData.copy());
        
        sendActorServerMessage(msg);
        
        //双向加最近互动
        ...
        ...
     
    }	

    //添加进成员列表
    team.putMemberData(memberData.getActorID(), memberData);
    //成员队伍反查字典(actorID:teamID)
    actorTeamDic.put(memberData.getActorID(),team.getId());

    //给自己推送
    BPWSEnterTeam msg2 = new BPWSEnterTeam(memberData.getActorID());
    msg2.setTeamData(team.copy());
    
    sendActorServerMessage(msg2);

    //已满
    if(team.isFull())
    {
        // 队伍满 清除申请列表
        clearTeamApplySet(team);
        
        if(clearTeamMatch)
        {
            //关了匹配
            if(team.isTeamMatching())
            {
                doCancelTeamMatch(team);
            }
        }
    }
}

移除个人匹配

成员序加1

先广播给队员有人加入队伍了(队伍加入信息)

添加进成员列表,成员队伍反查字典(actorID:teamID)

给自己推送(清自己队伍申请信息,刷AOI…和创建队伍给自己回复一样)

队伍满 清除申请列表,关匹配。

跟随

1.点击跟随

/** 开始跟随 */
public void startFollow()
{
    toChangeFollow(true,true);

    //清除召唤跟随默认同意倒计时cd
    agreeLaunchFollowCdTicker = 0;
}
/**
* 开始跟随
* @param isCheck 是否检查可跟随
* @param needNotice 是否通知
*/
private void toChangeFollow(boolean isCheck, boolean needNotice)
{
    //没有队伍
	if(!hasTeam())

    //队长不可主动切换跟随
	if(isLeader())

    //是否可跟随(场景是否可以跟随,副本情况处理,等等)
    int result = canStartFollow();

    //发送世界处理
}

主表判断是否可以跟随,

然后发送世界处理(因为队员和队长可能处于不同场景)

2.世界处理,尝试改变队伍成员跟随状态, 成功后广播给其他队员

public void memberTrySetTeamFollow(long memberID, boolean isFollowing)
{
    ...
    ...
    //获取队伍数据

    //拿到队长数据

    //根据队长所在场景,判断队员是否可以跟随
    int canFollowToScene(long memberActorID, int leaderSceneID, int leaderLineID)

    //可以跟随,迭代队伍所有成员,广播跟随成功

}

注意是根据队长所在场景,判断队员是否可以跟随

可以跟随,迭代队伍所有成员,广播到场景跟随成功

2.刷新队伍成员跟随状态

public void onRefreshTeamMemberFollow(long memberID, boolean isFollowing)
{
    //发送前端
    getActor().sendPacket(PacketIDConst.SCRefreshTeamMemberFollow,builder.build());

    //跟随状态变化
    onChangeFollow(memberID, isFollowing)
}

发送前端(角色id,是否跟随)

跟随状态变化:

/**
* 跟随状态变化
* @param isFollow true:跟随, false:取消跟随
*
* */
private void onChangeFollow(long memberActorID, boolean isFollow)
{
    /开始跟随的就是玩家自己
    if(getActor().getActorID() == memberActorID)
    {
        // 无论是刚进跟随还是取消跟随, 清理跟随子状态
        // 设置跑向跟随
        subFollowState = TeamFollowSubStatus.RunToFollow;

        if (isFollow)
		{
            // 离线状态跟随默认托管
            if (getActor().isWaitingReconnect())
            {
                ...
                ...
                //改变组队跟随托管状态为托管
                changeTeamFollowDepositToTrue();
            }
        }
    }
}

这里不是离线状态,会设置托管状态为–跑向跟随

玩家离线切至队长位置,然后设置托管状态为–托管跟随

队员跑到队长脚下改变状态为跟随托管

当队员跑到了队长脚下,前端会发起跟随托管协议。

1.组队跟随托管状态

/**
* 组队跟随托管状态
*
* @return
*/
public int teamFollowDeposit()
{
    //改变组队跟随托管状态为托管跟随
    changeTeamFollowDepositToTrue();

    ...
    ...

    // 如果队长在战斗状态, 则让队员取消托管
    changeTeamFollowDepositToFalse(TeamFollowSubStatus.FightFollow);
    ....
    ....
}

改变组队跟随托管状态为托管跟随

如果队长在战斗状态, 则让队员取消托管

2.改变组队跟随托管状态为托管跟随

/**
* 改变组队跟随托管状态为托管
*/
public void changeTeamFollowDepositToTrue()
{
    ...
    ...

    //是否曾经进入过托管状态(跟随需要设置为false, 托管的时候设置为true)
    hasEnterDepositOnce = true;

    //托管子状态设置为--托管跟随
    subFollowState = TeamFollowSubStatus.DepositFollow;

    //如果不是组队跟随托管状态, 需要的操作
    if (!isTeamFollowDeposit())
    {
        //发送组队跟随托管状态结果给前端
        sendTeamFollowDeposit(true);

        //设置跟随路点状态 队员速度和队长速度一致
        setMemberFollowLeaderPath(true);

        //组队跟随托管AI激活
        getActor().getTeamFollowAIModule().changeTeamFollowDeposit(true);
    }

    // 托管的时候检查上马
    operateRideUp();
}

设置跟随路点状态 队员速度和队长速度一致。

组队跟随托管AI激活(非常重要)。

再仔细看下设置跟随路点状态 队员速度和队长速度一致。

public void setMemberFollowLeaderPath(boolean isMemberFollowLeaderPath)
{
    ...
    ...
    AbstractBPScene scene = getActor().getScene();
    if(scene != null)
    {
        Actor leader = scene.getActorByActorID(teamData.getLeaderID());
        if(leader != null)
        {
            Knight currentKnight = leader.getKnightModule().getCurrentKnight();
            if(currentKnight != null)
            {
                //队长移动速度
                int leadSpeed = currentKnight.getAttribute(AttributeTypeEnum.MOVE_SPEED);
                //队长游泳移动速度
                int leadSwimSpeed = currentKnight.getAttribute(AttributeTypeEnum.SWIM_MOVE_SPEED);

                //将自己当前出战侠客移动速度和游泳速度同步为队长一致
                Knight currentKnightSelf = getActor().getKnightModule().getCurrentKnight();
                if(currentKnightSelf != null)
                {
                    currentKnightSelf.lockMoveAttribute(leadSpeed, leadSwimSpeed);
                }
            }
        }
    }

}

将自己当前出战侠客移动速度和游泳速度同步为队长一致

思考下,为什么这里不是拿actor的速度同步,而是拿出战侠客?

万一出战侠客变化,速度怎么改变?现在有操作没?

现在是侠客切换的时候:

public int swapWorkingAndBackup(KnightSwapTypeEnum swapTypeEnum)
[
    ...
    ...
    if(actor.getTeamModule().isLeader())
    {
        actor.getTeamModule().syncMemberSpeed();
    }
    ...
    ...
]

判断如果是队长,那么会重新锁定队员速度。

组队跟随托管AI(服务器驱动)

/**
* 组队跟随托管AI开始tick
*/
public void active()
{
    if (!isTick)
    {
        isTick = true;
    }

    //设置stop状态的interval
    //场景可达检测时间间隔
    checkStopInterval = SCENE_REACH_CHECK_INTERVAL;
}

1:看下有限状态机几个类设计

/**
 * 状态机管理基类
 */
public class BaseAIFsmManager<T extends AbstractCharacter>
{
    /**
     * 生物类引用
     */
    protected T owner;
}
由于玩家、npc...等等都会有状态机,所以生物类引用需要动态引入。

BaseAIFsmManager<T extends AbstractCharacter> 泛型动态传入,AbstractCharacter
是所有生物要基础的基类。

new BaseAIFsmManager<>(getCharacter());
new对象时传入生物对象。
/**
 * 状态机抽象类
 * 
 */
public abstract class AbstractAIState<E extends AbstractCharacter>
{

}
状态机抽象类,同样是泛型动态传入。

状态例如:组队离线跟随状态、组队idle状态、组队特殊被控制状态...都是一个状态,需要继承父类AbstractAIState.
/**
 *  离线跟随状态
 */
public class TeamFollowState extends AbstractTeamAIState
{

}
离线跟随状态需要继承状态机抽象类

2:组队有限状态机的初始化

**
 * 组队跟随托管AI
 * Created by yang dequan on 2018/8/21.
 */
public class TeamFollowAIModule extends AbstractCharacterModule
{
    @Override
    public void init()
    {
        // init 有限状态机
        fsmManager = new BaseAIFsmManager<>(getCharacter());

        //将组队离线跟随状态、组队idle状态、组队特殊被控制状态加入到列表
        List<AbstractAIState> stateSet = new ArrayList<>();
        stateSet.add(TeamFollowState.getInstance());
        stateSet.add(TeamIdleState.getInstance());
        stateSet.add(TeamSpecialControlState.getInstance());
        //初始化状态机为空闲状态
        fsmManager.init(StateEnum.IDLE, stateSet);

        //强控状态参数
        //多特殊状态共存的特殊控制参数类,多种控制状态可以共存,但是特殊表现状态还是只能有一个
        aiSpecialControlParam = AISpeicalcontrolParamFactory.create();
        aiSpecialControlParam.init();
    }
}

init 有限状态机,将组队离线跟随状态、组队idle状态、组队特殊被控制状态加入到列表,

初始化状态机为空闲状态,

初始化多特殊状态共存的特殊控制参数类

Search

    Table of Contents