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状态、组队特殊被控制状态加入到列表,
初始化状态机为空闲状态,
初始化多特殊状态共存的特殊控制参数类