怪物的AI策略

2020/11/02 Mmo-Game

怪物AI设计实现基于状态机:休闲AI、战斗AI、脚本AI

目录

表设计

有几个思想很重要:

1.模板表Npc, 可以简单理解为类型表。

2.实例表Npc_Stub_Editor关联Npc模板表,每个实例有自己特性,例如:
  出生坐标,朝向,基础AI...

3.实例表有配置AI列,模板表也有默认AI列,如果实例表不配置,那么默认模板
  表AI。相反,则以实例表配置AI为主。

Npc创建流程

1.初始化数据

1.NpcCreateModule创建Npc对象

private void toCreateObject(Npc npc, DictNpcData dictNpc, int teamLevel,int x,int y,int z,int rotation)
{
    ...
    //NPC初始化
    npc.init();
    npc.afterLoad();
    getScene().addBPObject(npc);

        //初始化怪物
    npc.afterInit();
    ...
}

NpcCreateModule创建Npc对象

2.Npc类下初始化属性npc.init();

1.初始化坐标,朝向…

2.初始化NPC数据表相关数据

private void initDictNpcAttr()
{
    赋值:
    DictNpcStub_editorData实例表
    DictNpcData模板表
    DictNpcBattleData怪物战斗表
}

3.initForceAndInteract()初始化阵营和交互类型

4.resetNpcAttr()设置npc属性(自适应等级!重要)

/**
* 设置NPC属性
*
* @param teamLevel 自适应等级(大于0 为自适应等级怪)
*/
public void resetNpcAttr(int teamLevel)
{
    1.如果接口传入怪物等级则以接口传入为准,否则以实例表
      配置的怪物等级为准,如果实例表没有配置怪物等级则以
      模板表怪物等级为准。
    2.怪物设置经验:
         a.如果NpcBattle配置了该级别的最大经验值字段maxExp,
           则根据(基础值、最大值、增幅)计算公式,设置经验。
         b.否则直接读NpcBattle配置的基础值。
    3.设置默认移动、姿态...
    4.根据等级属性系数类型和自适应等级获取属性组:
         a.NpcBattle配置了等级属性系数类型rateType
         b.int[] attrs = DictNpcLevelAttrData.getAttr(
           battleData.getRateType(), level)取出属性组
         c.NpcBattle和NpcLevelAttr根据公式计算属性
    5.根据NpcBattle添加固定属性
    6.补魔补蓝
    7.设置怪物伤害最小归属:
      由于抢怪判定,当玩家累计伤害超过该怪物的最大血万分比,则该怪物归属这个玩家
}

3.将npc添加到场景里

NpcCreateMoudule下:

getScene().addBPObject(npc);

4.NpcCreateMoudule下npc.afterInit();

1.初始化AI,initAi()

2.设置出生动画,派发无敌buff

3.根据NpcBirth表设置是否使用战斗姿态

AI模块初始化

1.创建NpcAiModule

创建npc时,会在成员变量下直接new一个NpcAiModule

/**
* @param npc
*/
public NpcAiModule(Npc npc)
{
    super(npc);
    owner = npc;
    battleFsmManager = new NpcAIFsmManager(npc);
    idleAIManager = new NpcIdleAIManager(npc);
    aiSpecialControlParam = AISpeicalcontrolParamFactory.create();
    aiSpecialControlParam.init();
    extraAiLogic = new ExtraAiLogicModule(npc);
    subStrategys = new ArrayList<>(1);
    waittingAddStrategys = new ArrayList<>(1);
    waittingRemoveStrategys = new TIntArrayList(1);
}

创建数据结构战斗状态机,空闲状态机...

2.初始化AI

NpcCreateMoudule下npc.afterInit(),

/**
* ai模块初始化
*/
private void initAi()
{
    Npc模板表和Npc_Stub_Editor实例表都会配置基础AI和扩展AI
    ,实例表优先级高。
    1.初始化扩展AI
    2.初始化基础AI
}
/** 初始化正常ai */
private void normalAiInit()
{
    //初始化空闲ai
    initIdleAi();

    if(owner.getNpcType() != NpcTypeEnum.FUNCTION)
    {
        //功能NPC不初始化
        initBattleFsmManager();
    }

    // 初始化拓展AI
    initExtraAi(owner);
}

3.空闲AI

1.初始化空闲ai

初始化空闲ai:
private void initIdleAi()
{
    //状态机初始化
    isIdle = true;

    //初始化空闲状态机
    idleAIManager.init(dictNpcCommonAi.getAiDatas());

    //初始化巡逻数据
    int patrolId = owner.getPatrolId();
    if(patrolId > 0)
    {
        patrolPath = DictNpcPatrolPathData.getDictNpcPatrolPathData(patrolId);
    }
}

dictNpcCommonAi.getAiDatas():
    NpcCommonAi表的idleIds字段配置空闲下状态id组,对应NpcIdleAiState表主键
初始化空闲状态机:NpcIdleAIManager
 idleAIManager.init(dictNpcCommonAi.getAiDatas());
    public void init(DictNpcIdleAiStateData[] aiStateData)
    {
        BPAiCache aiCache = npc.getScene().getAiCache();
        int length = aiStateData.length;
        this.aiStates = new IdleAiState[length];
        this.curStateIndex = length-1;

        //是否有条件为空的状态
        boolean conditionEmpty = false;
        //初始化条件
        conditions = new boolean[length][];
        for (int i = 0; i < length; i++)
        {
            DictNpcIdleAiStateData stateData = aiStateData[i];
            //从池里拿出一个状态(这里利用了对象池的技术)
            IdleAiState aiState = aiCache.getIdleAiState();
            //重新初始化状态数据
            aiState.init(npc,this, i, stateData);
            aiStates[i] = aiState;
            //根据状态 获取 条件组
            DictNpcIdleAiConditionData[] conditionArray = stateData.getConditionArray();
            int size = conditionArray.length;
            //迭代每个状态的条件组
            for (int j = 0; j < size; j++)
            {
                //一个条件
                DictNpcIdleAiConditionData data = conditionArray[j];
                //一个条件关联的事件枚举组(例如:温度变化超出范围、温度变化回归正常)
                AIEventEnum[] eventEnum = data.getConditionEnum().getEventEnum();
                if(eventEnum != null)
                {
                    for (int m = 0, n = eventEnum.length; m < n; m++)
                    {
                        AIEventEnum aiEventEnum = eventEnum[m];
                        TIntArrayList list = eventToCondition.get(aiEventEnum);
                        if (list == null)
                        {
                            list = new TIntArrayList();
                            eventToCondition.put(aiEventEnum, list);
                        }
                        list.add(i);
                        list.add(j);
                    }
                }
            }
            //有条件为空的,将检测标识设置为true
            if(size == 0 && i < length-1)
            {
                isDirty = true;
            }
        }
        if(curStateIndex >= 0)
        {
            //初始化第一个状态
            IdleAiState aiState = aiStates[curStateIndex];
            if(aiState.getAiAction(0) != null)
            {
                aiState.setCurActionIndex(0);
            }
        }
    }
Npc空闲状态IdleAiState初始化:
/**
* 初始化空闲状态
* @param owner 生物
* @param idleAIManager 状态机管理器
* @param stateIndex 状态索引
* @param stateData 状态表数据
*/
public void init(T owner,NpcIdleAIManager idleAIManager, int stateIndex, DictNpcIdleAiStateData stateData)
{
     //状态条件组
    DictNpcIdleAiConditionData[] conditionArray = stateData.getConditionArray();
    //进入条件组长度
    int length = conditionArray.length;
    conditions = new AbstractAiCondition[length];
    for (int i = 0; i < length; i++)
    {
        DictNpcIdleAiConditionData conditionData = conditionArray[i];
        //根据类型创建对应的条件
        conditions[i] = AiConditionFactory.getCheckCondition(conditionData.getConditionEnum());
        //添加触发器,每个条件会有一个类型,对应表里的type字段
        //根据type会创建对应trigger
        idleAIManager.addTrigger(stateIndex, i, conditionData);
    }

    BPAiCache aiCache = owner.getScene().getAiCache();
    //执行行为组
    DictNpcIdleAiActionData[] actionsArray = stateData.getActionsArray();
    //状态切换,执行行为 数量
    int capacity = actionsArray.length;
    length = actionsArray.length;
    //创建执行行为组
    actions = new AbstractAiAction[length];
    for (int i = 0; i < length; i++)
    {
        DictNpcIdleAiActionData actionData = actionsArray[i];
        //从池里拿出执行行为
        AbstractAiAction aiAction = aiCache.getAiAction(actionData.getType());
        //todo todo 可能要判下空
        //重新初始化行为数据
        aiAction.init(owner, stateData, actionData);
        actions[i] = aiAction;

        List compositeActions = aiAction.getCompositeActions();
        if(compositeActions != null)
        {
            capacity += compositeActions.size();
        }
    }
    //状态切换,执行行为 (已经执行过, 不再执行)
    changeActions = new AbstractAiAction[capacity];
    for (int i = 0,m = 0, size = actionsArray.length; i < size; i++)
    {
        DictNpcIdleAiActionData actionData = actionsArray[i].getChangeActionData();
        if(actionData != null)
        {
            AbstractAiAction aiAction = aiCache.getAiAction(actionData.getType());
            aiAction.init(owner, stateData, actionData);
            changeActions[m++] = aiAction;

            //复合状态处理
            List<AbstractAiAction> compositeActions = aiAction.getCompositeActions();
            if(compositeActions != null)
            {
                i++;
                for (int j = 0, s = compositeActions.size(); j < s; j++)
                {
                    AbstractAiAction action = compositeActions.get(i);
                    AbstractAiAction changeAction = aiCache.getAiAction(action.getActionData().getChangeActionData().getType());
                    changeAction.init(owner, stateData, actionData);
                    changeActions[m++] = changeAction;
                }
            }
        }
    }

}

运用到了对象池,可以再深入探究

1.根据类型创建对应的条件组
2.添加触发器
3.创建执行行为组
4.状态切换,执行行为 (已经执行过, 不再执行)
5.退出行为组

2.类主要成员

/**
* 空闲状态机管理器
*
/
public class NpcIdleAIManager
{
     private Npc npc;

    /**
     * 状态表格数据组
     * 后面的优先级最低
     */
    private IdleAiState[] aiStates;

    
    /**
     * 当前运行的状态索引
     *
     * (因为状态组是后面的优先级高,所以当前状态索引从length - 1开始)
     */
    private int curStateIndex;

     /**
     * 条件标志是否更新
     */
    private boolean isDirty;

    /**
     * 各个状态的条件状态
     *
     * 状态索引-->条件索引-->是否触发
     */
    private boolean[][] conditions;

     /**
     * 条件触发器
     */
    private List<AbstractAiConditionTrigger> triggers = new ArrayList<>();

    
    /**
     * key事件类型,value 关联的条件索引 (状态索引,条件索引) 一组
     */
    private EnumMap<AIEventEnum,TIntArrayList> eventToCondition = new EnumMap<>(AIEventEnum.class);
}

核心思想,状态机根据当前维护的状态索引,执行对应状态的行为。如果,条件达成,判断

是否切入下一个状态机。

eventToCondition事件組,接受外部过来的事件,当事件过来判断关联的条件状态是否满足,满足

则做相应变化。

//抽象的ai条件触发器
public abstract class AbstractAiConditionTrigger
{
    protected NpcIdleAIManager idleAIManager;

    /**
     * 状态索引
     */
    protected int stateIndex;

    /**
     * 条件索引
     */
    protected int conditionIndex;

    public void tick(int interval)
    {
        tickCallback(interval);
    }


    public abstract void tickCallback(int interval);

    /**
     * 监听器的回调
     * @param param0
     * @param param1
     * @return
     */
    public void listenerCall(int param0, int param1)
    {
    }
}
条件触发器里主要是:
1.当前条件触发器属于哪个状态以及哪个条件索引。

2.有tick调用接口,但trigger本身并不进行tick,是由空闲状态管理者统一管理,
  这么做非常好,可以解决自身tick浪费性能问题。例如多条件的与关系,如果空
  闲状态管理者tick trigger列表的时候,有一个条件不满足则可以立即跳出,节
  省性能。目前当条件tirgger满足后,是将对应的二维数组状态标识置为true,空
  闲状态管理者通过检查二维数组判断当前状态下所有的条件是否都为true来判断是
  否进入该状态。

3.监听器的回调listenerCall,条件支持了外部事件改变状态,很好的设计,有
  必要。 
/**
 * NPC空闲ai 状态数据
 */
public class IdleAiState<T extends AbstractCharacter> implements Reusable
{
    /**
     * 进入条件
     */
    private AbstractAiCondition[] conditions;

    /**
     * 执行行为
     */
    private AbstractAiAction[] actions;

    /**
     * 状态切换,执行行为 (已经执行过, 不再执行)
     */
    private AbstractAiAction[] changeActions;

    /**
     * 状态切换时,退出执行的行为id (一定执行)
     */
    private AbstractAiAction[] exitActions;

    /**
     * 当前运行行为索引
     */
    private int curActionIndex = -1;
}

进入该状态的条件组,执行行为组…

//条件
public abstract class AbstractAiCondition<E extends AbstractCharacter>
{
     /**
     * 条件检查
     */
    public final boolean onAIEvent(E owner, AIEventEnum eventEnum,DictNpcIdleAiConditionData conditionData, int param0, int param1)
    {
        return toOnAIEvent(owner, eventEnum, conditionData,param0,param1);
    }

    /**
     * 检查逻辑
     * @param eventEnum
     * @param param0
     * @param param1
     */
    protected abstract boolean toOnAIEvent(E owner, AIEventEnum eventEnum,
                                           DictNpcIdleAiConditionData conditionData, int param0, int param1);

    /**
     * 行为执行完成,是否需要清空 条件标志
     */
    public boolean isClearMark()
    {
        return false;
    }

}

提供统一的外部触发事件接口,子类覆写检测条件是否达成。

该条件也是状态下的条件组成一部分,例如上功能和ConditionTrigger有重复,

后来确认后,最主要原因是因为AiCondition是单例,而AiConditionTrigger因为很多参数,没发做成

单例,所以单独出来的一个AiCondition概念。

//行为
public abstract class AbstractAiAction<T extends AbstractCharacter> implements Reusable
{
      /**
     * 是否执行
     * 默认执行,若执行过,则不在执行
     */
    protected boolean isRun;

    /**
     * 当前行为是否完成
     */
    protected boolean isComplete;


    //执行行为
    public final void executeAiAction(IdleAiState idleAIState, int interval)
    {

    }

      /**
     * 打断行为
     */
    protected AbstractAiAction breakAction;


}

行为分为持续性行为和立即行为!! 如果当前状态被打断,则需要打断当前行为,当前行为需要做一些

事情(例如:移动那么需要停止),同时需要根据配置执行打断要做的行为。

总结:
大体就是上面几个类,可以把它们理解为组件,组件分为:
    状态机管理组件、状态组件、条件组件、行为组件。

状态机管理器:管理所有状态,同时注册所有事件。外部触发的事件,都是由状态机管理先接受然后派发
             到对应状态。
             状态机管理器下注册的事件目前有两种:ConditionTrigger和AiCondition。

状态:注册自己关心的事件,管理所有条件和行为。条件是ConditionTrigger和AiCondition都满足了才能
     切为当前状态。

事件:如果是会动态变化的事件,例如:天气,  这种的事件都是基于外部事件立即触发的,即使天气改变了
     也是外部立即触发过来,然后状态机管理器统一tick的时候,会检测条件在当前时刻是否满足,所以可
     以解决动态变化事件问题。

3.空闲ai执行流程

/**
* tick检查
*/
public void tick(int interval)
{

    //触发器
    if(!triggers.isEmpty())
    {
        for (int i = 0, size = triggers.size(); i < size; i++)
        {
            triggers.get(i).tick(interval);
        }
    }
    //先检测当前状态的条件,ai条件检查
    checkAiCondition();

    //则执行当前状态的行为
    IdleAiState aiState = getIdleAIState(this.curStateIndex);
    if(aiState != null)
    {
        aiState.executeAiAction(interval);
    }
}
/**
* 检查各个状态条件是否满足
* 是否需要切换状态
*/
private void checkAiCondition()
{
    if(!isDirty)
    {
        return;
    }

    boolean result = false;
    //状态组的状态索引
    int index = 0;
    int curStateIndex = this.curStateIndex;
    first:
    //后面的优先级最低,所以前往后遍历
    for (int i = 0, size = curStateIndex; i <= size; i++)
    {
        boolean[] condition = conditions[i];
        //只有有一个条件不满足则会跳出到first点
        for (int j = 0, length = condition.length; j < length; j++)
        {
            if(!condition[j])
            {
                continue first;
            }
        }
        //一个状态下的所有条件达成则可以跳出,因为前面的优先级比较高
        result = true;
        index = i;
        break;
    }
    if(result)
    {
        //状态条件满足
        //(因为状态组是后面的优先级更低,所以当前状态索引从length - 1开始)
        if(index < curStateIndex)
        {
            //执行下一状态
            executeNextState(index);
        }
    }
    //所有条件不满足,并且当前不是初始状态,那么需要切换初始状态
    else if(curStateIndex != (aiStates.length-1))
    {
        //所有条件都未不满足,切换到第一个行为
        resetState();
    }
    isDirty = false;
}
//则执行当前状态的行为
IdleAiState aiState = getIdleAIState(this.curStateIndex);
if(aiState != null)
{
    aiState.executeAiAction(interval);
}

4.战斗AI

每个NPC都初始化一个状态机管理器;
通过tick,运行更新当前状态。Tick中可以进行状态切换;
游戏过程中由于玩家某些操作也会产生状态切换;

初始化战斗状态机管理器入口:

/** 初始化正常ai */
private void normalAiInit()
{
    if(owner.getNpcType() != NpcTypeEnum.FUNCTION)
    {
        //功能NPC不初始化
        initBattleFsmManager();
    }
}

功能npc不初始化战斗状态机。

1.初始化流程

private void initBattleFsmManager()
{
    // 初始化AI
    List<AbstractAIState<Npc>> stateSet = new ArrayList<>();
    stateSet.add(NpcNoneState.getInstance());
    stateSet.add(NpcBattleState.getInstance());
    stateSet.add(NpcPursuitState.getInstance());
    stateSet.add(NpcResetState.getInstance());
    stateSet.add(NpcForceControlRecoverState.getInstance());
    stateSet.add(NpcSpecialControlState.getInstance());
    stateSet.add(NpcAlertState.getInstance());
    stateSet.add(NpcStoicState.getInstance());
    stateSet.add(NpcGroupState.getInstance());
    battleFsmManager.init(StateEnum.NONE, stateSet);
}

初始化战斗状态机,默认是空状态!! 添加npc的各个状态。

/**
    * @param initalState 初始化状态机类型
    * @param stateList
    */
public void init(StateEnum initalState, List<AbstractAIState<T>> stateList)
{
    if(stateList == null)
    {
        BPLog.BP_LOGIC.error("AI BaseAIFsm init error");
        return;
    }

    for (int i = stateList.size() - 1; i >= 0; i--)
    {
        AbstractAIState<T> aiState = stateList.get(i);
        //npc战斗所有状态
        stateMap[aiState.getKey().getValue()] = aiState;
    }

    //初始化当前状态
    curStatEnum = initalState;
    curState = stateMap[initalState.getValue()];
}

2.类的主要属性

/**
 * 状态机管理类
 */
public class BaseAIFsmManager<T extends AbstractCharacter>
{
     /**
     * 生物类引用
     */
    protected T owner;

    /**
     * 当前状态枚举
     */
    protected StateEnum curStatEnum = StateEnum.NONE;

    /**
     * 当前状态
     */
    protected AbstractAIState<T> curState;

    /**
     * 状态枚举-状态实例 映射
     */
    protected AbstractAIState<T>[] stateMap = new AbstractAIState[StateEnum.values().length];

     /**
     * 转换状态  ( 当前帧不会转换,下一帧才转换)
     *
     * @param stateEnum
     */
    public boolean transtTo(StateEnum stateEnum)
    {

    }

      /**
     * Tick
     */
    public void tick(int interval)
    {

    }

      /**
     * event
     *
     * @param aiEvent
     * @param inParam0
     * @param intParam1
     * @param stringParma
     * @param objectParam
     */
    public void onAIEvent(AIEventEnum aiEvent, int inParam0, int intParam1, String stringParma, Object objectParam)
    {

    }

}
状态机管理基类:

几个方法:
  1.tick(int interval)
  2.接受外来事件onAIEvent()
  3.转换状态transtTo()

类成员:
  1.当前状态curState
  2.所有状态stateMap
  3.对象owner
/**
 * NPC扩展状态切换管理器,NPC有些特殊逻辑
 *
 * @author wangguohao
 * @time 2018/7/3 - 13:15
 */
public class NpcAIFsmManager extends BaseAIFsmManager<Npc>
{
    /**
     * NPC特殊处理
     * 切换前检查, 是否可以进入状态
     * @param stateEnum
     * @return
     */
    @Override
    public boolean transtTo(StateEnum stateEnum)
    {

    }
}

子类NpcAIFsmManager重写了transTo()方法,里面逻辑没有抽象很好,

有很多重复逻辑 父类可以抽象一个公共子类方法,让子类去覆写进行特殊检测。

/**
 * 状态抽象类
 * 
 * @author wangguohao
 * @time 2017年7月6日 - 下午5:00:08
 */
public abstract class AbstractAIState<E extends AbstractCharacter>
{
    /**
	 * 当前状态可转换的状态
	 */
	protected EnumSet<StateEnum> transitionState = EnumSet.noneOf(StateEnum.class);

     /**
     * 判断是否可以进入此状态
     * @param e
     * @return
     */
	public boolean canEnter(E e)
    {
        return true;
    }

	/**
	 * 进入当前状态要做的事
	 * @param e
	 * @return
	 */
	public abstract boolean enter(E e);

    /**
     * 判断是否可以退出此状态
     * @param e
     * @return
     */
    public boolean canExit(E e)
    {
        return true;
    }

	/**
	 * 退出当前状态要做的事
	 * @param e
	 * @return
	 */
	public abstract boolean exit(E e);

	/**
	 * tickBySceneService
	 * @param e
	 * @param interval
	 * @return true代表要切换状态,子类不继续走tick
	 */
	public abstract void tick(E e, int interval);

	/**
	 * AI状态机处理外部事件
	 * @param e
	 * @param aiEvent
	 * @param intParam0
	 * @param intParam1
	 * @param stringParma
	 * @param objectParam
	 */
	public abstract void onEvent(E e, AIEventEnum aiEvent,int intParam0,int intParam1,String stringParma,Object objectParam);
}
状态抽象类:
  方法:
   1.能否进入canEnter
   2.进入要做的事enter
   3.能否退出canExit
   4.退出要做的事exit
   5.tick
   6.接受外部事件onEvent

 类成员:
   1.当前状态可转换的状态transitionState,
     也就是每个状态在初始化时都会注册当前状态可以切换到其它
     状态的列表。

3.战斗ai接收事件

NPCAiModule下:

/** 正常ai 接收事件 */
private void normalAiEvent(AIEventEnum aiEvent, int inParam0, int intParam1, String stringParma, Object objectParam)
{
    /战斗相关事件(传入的事件不是空闲状态机关系的事件,和控制状态移除)
    if(aiEvent != AIEventEnum.AI_EVENT_STATE_REMOVE && !aiEvent.getIsIdle())
    {
        Npc npc = (Npc)getCharacter();
        //木桩怪直接返回,或者不可以攻击
        if(!npc.canAttack() || npc.getNpcType() == NpcTypeEnum.FUNCTION)
        {
            //脱战
            npc.getFightModule().secedeFight();
            //清空攻击目标数据,
            npc.getEnmityManager().destory();
            return;
        }

        if(isIdle)
        {
            //战斗状态
            switchFsmManager();
            //不为idle了
            isIdle = false;
        }
    }
}

1.不可攻击或者功能npc接受到战斗AI事件,直接脱战和清空仇恨组返回。

2.如果当前是空闲状态,切换战斗状态switchFsmManager(),设置isIdle = false。
   /**
     * 切换状态机
     * 重置相关参数, 再修改标志
     */
    public void switchFsmManager()
    {
        if(isIdle)
        {
            idleAIManager.resetState();
            isIdle = false;

            //通知前端,处于战斗待机
            switchBattlePose(true);

            //往场景上抛事件
            Npc owner = this.owner;
            AbstractBPScene scene = owner.getScene();
            if (null != scene)
            {
                int attackObjectId = owner.getEnmityManager().getAttackObjectId();
                scene.getSceneFlowModule().callSceneListener(SceneTriggerTypeEnum.ACTOR_ENTER_NPC_VIEW, attackObjectId, owner.getSceneGuid());

                //召唤其他组怪物进入战斗
                callOtherGroup(attackObjectId);
            }

            //首次进入战斗ai,设置攻击间隔(首次立即攻击)
            resetSkillInterval();
        }
    } 
    通知前端,处于战斗待机。
    往场景上抛事件。
    召唤其他组怪物进入战斗。
    首次进入战斗ai,设置攻击间隔(首次立即攻击)
/** 正常ai 接收事件 */
private void normalAiEvent(AIEventEnum aiEvent, int inParam0, int intParam1, String stringParma, Object objectParam)
{
    //事件触发
    if(isIdle)
    {
        idleAIManager.onAIEvent(aiEvent, inParam0,intParam1);
    }
    else
    {
        //战斗状态下,空闲ai也要接受相关空闲事件
        //例如,战斗下天气改变,空闲ai不接收事件,就无法改变条件状态。
        //      防止退出战斗状态变为空闲但是天气已经变了,空闲状态执行行为不对。
        if(aiEvent.getIsIdle())
        {
            //如果战斗状态下发生了空闲状态关心的事件,需要触发空闲事件
            idleAIManager.onAIEvent(aiEvent, inParam0,intParam1);
        }
        //旧的状态
        StateEnum oldStatEnum = battleFsmManager.getCurStatEnum();
        //当前状态接受事件
        battleFsmManager.onAIEvent(aiEvent, inParam0, intParam1, stringParma, objectParam);
        if(oldStatEnum != StateEnum.RESET && battleFsmManager.getCurStatEnum() == StateEnum.NONE)
        {
            //如果上一状态不是脱战状态,且当前状态为NONE,切换到ALERT
            transtTo(StateEnum.ALERT);
        }
    }
}
1.当前状态battleFsmManager.onAIEvent()接受事件,对应做改变。
2.如果上一状态不是脱战状态,且当前状态为NONE,切换到警戒状态。

4.各个战斗状态的执行逻辑

1.战斗下警戒状态

NpcAlertState extends AbstractNpcAIState

1.警戒下可转换的状态初始化:

private NpcAlertState()
{
    clearTransitionStates();
    addTransitionState(StateEnum.BATTLE);
    addTransitionState(StateEnum.RESET);
    addTransitionState(StateEnum.SPECIAL_CONTROL);
    addTransitionState(StateEnum.FORCE_CONTROL_RECOVER);
    addTransitionState(StateEnum.PURSUIT);
    addTransitionState(StateEnum.STOIC);
    addTransitionState(StateEnum.GROUP);
}
战斗状态、复位状态、特殊控制状态、强控恢复硬直、追击状态
霸体破除、组策略状态

2.进入要做的事:

@Override
public boolean enter(Npc npc)
{
    if (!super.enter(npc))
    {
        return false;
    }

    //根据npc获取怪物ai模块
    NpcAiModule aiManager = npc.getNpcAiModule();

    DictNpcCommonAi commonAi = aiManager.getDictNpcCommonAi();
    //是否可以召唤周围怪物, 如果怪物有目标,则返回
    if(commonAi.getIsHelp() == NpcConstant.NPC_CAN_HELP)
    {
        //设置同一仇恨列表的npc攻击目标
        setAttackTarget(npc);
    }
    
    //对于首次必放技能,判断是否可以立即使用(保证同一帧同时放技能)
    enterUseSkill(npc);

    //检测是否在水中倒计时
    aiManager.initCheckWaterInterval();

    return true;
}

//对于首次必放技能,判断是否可以立即使用(保证同一帧同时放技能)
private boolean enterUseSkill(Npc npc)
{
    NpcAiModule aiManager = npc.getNpcAiModule();
    //是第一个使用技能 , 能够使用技能, 不在技能持续时间
    if(aiManager.isFirstUseSkill() && aiManager.canUseSkill() && !aiManager.isSkillRemainTime())
    {
        //检查选技能逻辑
        //返回技能范围平方[<=0 无追击,不释放技能]
        int distancePow = aiManager.checkExtraLogic();
        if (distancePow > 0)
        {
            EnmityManager enmityManager = npc.getEnmityManager();
            //判断敌人是否在技能返回内,在那么npc直接切换到战斗状态
            AbstractCharacter target = enmityManager.checkTarget(enmityManager.getAttackObjectId());
            if(target != null && distancePow > MapUtils.distanceSqBetweenPos(npc.getX(),npc.getZ(),target.getX(),target.getZ()))
            {
                //切换到战斗状态
                aiManager.transtTo(StateEnum.BATTLE);
                return true;
            }
        }
    }
    return false;
}

3.tick要做的事:

1.增加技能间隔。
2.判断是否有攻击目标对象,没有则切换到复位状态。
3.如果大于出生地直接切到复位状态。
4.如果在水中直接切到复位状态。
5.计算怪与玩家的距离,如果在技能范围内,切换到战斗状态。
6.否则走包围逻辑。

4.退出要做的事:

1.如果在行走则停止。
2.清除检测是否在水中倒计时。
2.复位状态

1.可转换的状态:

1.死亡
2.警戒

2.进入要做的事:

1.buff清理
2.情况扩展ai
3.不是默认战斗姿态,则通知前端,取消战斗待机
4.NPC主动调用脱战
5.补血补蓝,填满霸体,回调场景事件

3.tick要做的事:

1.判断是否到达出生点,如果到达:
    重置为出生朝向。
    NPC恢复空闲状态事件。
2.如果未达到出生点且不在移动则调用寻路往出生地走。

4.接收事件:

public void onEvent(Npc npc, AIEventEnum aiEvent, int intParam0, int intParam1, 
String stringParma, Object objectParam)
{
    NpcAiModule aiManager = npc.getNpcAiModule();
    if(aiEvent == AIEventEnum.AI_EVENT_NPC_IDLE)
    {
        aiManager.switchFsmManager();
        return;
    }
}

如果接收到npc空闲恢复状态则切换状态机。

3.战斗状态

1.可转换的状态:

1.死亡
2.追击
3.复位
4.特殊控制状态
5.强控恢复硬直
6.战斗下警戒
7.霸体破除
8.QTE状态
9.组策略状态

2.进入要做的事:

执行选技能逻辑

3.tick要做的事:

1.如果大于出生地,则直接返回
2.选技能失败,切换到警戒状态
3.选技能成功并且技能释放完成,切换到警戒状态
4.追击状态

1.可转换的状态:

省略

2.tick要做的事:

1.在追击过程中,目标丢失:
  检测仇恨组其他目标,不为空则切换到警戒,
  为空则切换到复位。
2.超出出生地一定距离,切换复位。
3.能移动并且不在rvo壁障:
  若目标点移动,导致站位圈坐标清除,则切入警戒状态

具体细节可以单独再看
5.霸体槽打空后状态

当霸体槽被打空后事件触发,进入改状态。

1.接收事件:
 霸体槽打空后的状态结束切换为警戒状态
 超出出生点过远切换为复位状态
6.npc死亡后状态

1.可转换的状态:

战斗下警戒状态

2.进入要做的事:

1.重置仇恨管理器相关参数。
2.触发扩展ai死亡事件

3.tick要做的事:

死亡从AOI中移除
7.npc组策略状态

1.进入要做的事:

清除圈层信息

2.tick要做的事:

1.添加组策略列表
2.执行组策略列表
3.删除组策略列表

3.接收事件:

超出出生点过远,切换为复位状态。
8.NPC的特殊控制状态

1.进入要做的事:

1.停止正常移动,不停止特殊移动
2.停止技能

2.tick要做的事:

检测是否需要切入强控恢复硬直。

3.退出要做的事:

停止移动,停止技能,清空被控参数

5.脚本AI

1.脚本AI的初始化

1.表数据初始化

1.构建Map<String, Class> classMap数据结构

1.DictNpcStub_editorData:
public void init(List<IDictBase> l)
{
    super.init(l);

    if(!checkScriptClass())
    {
        return;
    }
}

脚本目录下,脚本类检测。
同时扫描脚本包路径下所有类,初始化到DictNpcStub_editorData类下成员数据结构
/**
    * 脚本名对应的类实例
    * key为 名字 , value为类实例
    */
public static Map<String, Class> classMap;

2.读取Map<String, Class> classMap

@Override
public void init(List<IDictBase> l)
{
    if(record.getScriptId() > 0)
    {
        //根据类名读取对应的类
        Class aClass = classMap.get(CLASS_PREFIX + record.getScriptId());
        if(aClass == null)
        {
            check = false;
            BPLog.BP_SYSTEM.error("种怪表 未找到脚本对应类 怪物guid:{} 文件全名:{} 文件id:{}",
                    record.getSceneGuid(),"com.game2sky.script.ai.Script"+record.getScriptId(), record.getScriptId());
            return;
        }
        record.setScriptClass(aClass);
    }
}
根据类名读取对应的类

3.如果种怪表没找到,则从npc表读取对应类,原理类似。

2.NpcAIModule下初始化
 //获取脚本class类
Class<?> script = owner.getScript();
if(script != null)
{
    isScript = true;
    try
    {
        //获取脚本实例
        aiScript = (AbstractAIScript) script.newInstance();
        //初始化脚本状态机
        aiScript.init(owner);
        if(dictNpcCommonAi != null)
        {
            //扫描敌人的最大距离(boss脚本ai使用)
            autoDistance = dictNpcCommonAi.getTargetDistance();
        }
    } catch (InstantiationException e)
    {
        BPLog.BP_SYSTEM.error("script error scriptName:{} {}", script.getName(), ExceptionUtils.exceptionToString(e));
        return;
    } catch (IllegalAccessException e)
    {
        BPLog.BP_SYSTEM.error("script error scriptName:{} {}", script.getName(), ExceptionUtils.exceptionToString(e));
        return;
    }
    if(null != dictNpcCommonAi && dictNpcCommonAi.getOpenIdleAi())
    {
        initIdleAi();
    }
}
3.初始化脚本状态机
AbstractAIScript:
@Override
public void init(Npc npc)
{
    this.npc = npc;
    isScriptTick = true;
    manager = new ScriptFSMManager(npc, this);
}

public ScriptFSMManager(Npc npc,AbstractAIScript aiScript)
{
    this.npc = npc;
    this.aiScript = aiScript;
    buildScriptState();
}

/**
* 构建脚本状态机的状态
*/
private void buildScriptState()
{
    ScriptBirthState birth = new ScriptBirthState(this);
    states[birth.getStateEnum().getIndex()] = birth;
    ScriptIdleState idle = new ScriptIdleState(this);
    states[idle.getStateEnum().getIndex()] = idle;

     //脚本战斗状态是一个父状态会有很多子状态
    ScriptBattleState battle = new ScriptBattleState(this);
    states[battle.getStateEnum().getIndex()] = battle;

    ScriptResetState reset = new ScriptResetState(this);
    states[reset.getStateEnum().getIndex()] = reset;

    this.state = birth;
    this.stateEnum = state.getStateEnum();
    //同时立即调用一次,出生状态进入操作
    birth.enter();
}

首先NpcAiModule下挂着boss脚本AI管理器抽象类AbstractAIScript。
//初始化脚本状态机管理者
aiScript.init(owner);
主要就是new一个ScriptFSMManager脚本状态机管理者,同时构造脚本状态。

2.各个类的主要成员属性

脚本AI,由程序提供底层架构支持,抽象各种行为,策划可以组合接口,来实现需求

1.挂在NpcAiMoudule下的AbstractAIScript
/**
 * AI脚本抽象类
 * @time 2019/9/4 - 10:11
 */
public class AbstractAIScript implements IScript
{
     /** 脚本归属 */
    protected Npc npc;

    /** 脚本状态机 */
    protected ScriptFSMManager manager;

    /** NPC是否被控制 只控制脚本逻辑是否运行 */
    protected boolean isScriptTick;

     @Override
    public void init(Npc npc)
    {

    }

     @Override
    public void tick(int interval)
    {

    }

     @Override
    public void onEvent(AIEventEnum event, int intParam0, int intParam1, String stringParma, Object objectParam)
    {

    }

    ...

    //给策划脚本提供的开发接口
    1.出生状态进入
    2.进入空闲状态
    3.空闲状态持续
    4.空闲状态 退出
    5.进入战斗状态
    ...
}

主要:
    1.脚本状态机ScriptFSMManager
    2.init,tick,onEvent等方法
    3.给策划脚本提供的开发接口
2.脚本状态机ScriptFSMManager
/**
 * 脚本状态机 受脚本对象控制
 */
public class ScriptFSMManager
{
    /**
     * 脚本对象
     */
    private AbstractAIScript aiScript;

    /**
     * 状态机当前状态
     */
    private AIScriptStateEnum stateEnum = AIScriptStateEnum.BIRTH;

    /**
     * 当前状态
     */
    protected AbstractScriptState state;

    /**
     * 拥有的状态
     */
    protected AbstractScriptState[] states = new AbstractScriptState[AIScriptStateEnum.values().length];

    /**
     * 构建脚本状态机的状态
     */
    private void buildScriptState()
    {

    }

     /**
     * 当前状态的tick
     */
    public void tick(int interval)
    {
        if(curState != null)
        {
            curState.tick(interval);
        }
    }

     /**
     * 状态切换
     * @param stateEnum 要切换到的状态
     */
    public boolean switchState(AIScriptStateEnum stateEnum)
    {
    }

    /**
     * 当前状态事件触发
     */
    public void onEvent(AIEventEnum event, int intParam0, int intParam1, String stringParma, Object objectParam)
    {

    }
}
主要:
    1.类成员:
      脚本对象aiScript、状态机当前状态curState、
      拥有的状态列表states
    2.方法:
      构建脚本状态机的状态buildScriptState(),tick(),
      状态切换switchState(),当前状态事件触发onEvent()

3.脚本状态抽象类AbstractScriptState
/**
 * 脚本状态抽象类
 */
public abstract class AbstractScriptState
{
    /**
     * 脚本管理器
     */
    protected ScriptFSMManager manager;

    public void enter()
    {
    }

    public void tick(int interval)
    {

    }

    public void exit()
    {
    }

    public void onEvent(AIEventEnum event, int intParam0, int intParam1, String stringParma, Object objectParam)
    {

    }
}
主要:
    类成员:脚本管理器
    方法:enter,tick,exit,onEvent
4.脚本状态

1.战斗状态下子状态包括:强控状态、强控恢复状态;
2.状态的进入、tick、退出,都会调用脚本接口,驱动脚本逻辑;
3.子状态会关联一个父状态,父状态会进行子状态间的切换;
4.脚本工程的脚本类会继承游戏服的AbstractAIScript抽象类。

3.执行流程

1.状态机tick流程

NpcAiModule:

if(isTick)
{
    if(isScript)
    {
        //脚本下,也需要tick
        EnmityManager enmityManager = owner.getEnmityManager();
        enmityManager.tick(tickInterval);

        aiScript.tick(tickInterval);
    }
    else
    {
        normalAiTick(tickInterval);
    }
}
脚本调用 aiScript.tick()方法,各个子状态进行tick逻辑。

2.状态切换要做的

ScriptFSMManager:
/**
* 状态切换
* @param stateEnum 要切换到的状态
*/
public boolean switchState(AIScriptStateEnum stateEnum)
{
    //如果当前状态等于要进入的状态,直接返回
    AIScriptStateEnum curState = this.stateEnum;
    if (curState == stateEnum)
    {
        return true;
    }
    BPLog.BP_SYSTEM.debug("switchState curState:{} targetState:{}",this.stateEnum, stateEnum);

    //校验状态是否越界
    int index = stateEnum.getIndex();
    if (index < 0 || index >= states.length)
    {
        BPLog.BP_SYSTEM.error("switchState error curState:{} targetState:{}",
                this.stateEnum, stateEnum);
        return false;
    }

    //当前状态不为空则退出
    if (this.curState != null)
    {
        //退出要做的事
        this.curState.exit();
    }

    //状态切换,默认打断相关行为(移动,技能)
    breakAction();

    //切换状态回调
    aiScript.switchStateCallback();

    AbstractScriptState subState = states[index];
    if (subState == null)
    {
        BPLog.BP_SYSTEM.error("switchState error state no find targetState!!! curState:{} targetState:{}",
                this.stateEnum, stateEnum);
        return false;
    }
    //进入新状态要做的事
    subState.enter();
    this.curState = subState;
    this.stateEnum = stateEnum;
    return true;
}
1.如果当前状态等于要进入的状态,直接返回
2.校验状态是否越界
3.当前状态退出要做的事
4.状态切换,默认打断相关行为(移动,技能)
5.切换状态回调
6.进入新状态要做的事

3.ScriptBirthState出生状态

1.进入要做的事:

 /**
* 构建脚本状态机的状态
*/
private void buildScriptState()
{
    ...
     //同时立即调用一次,出生状态进入操作
     birth.enter();
}
在构建脚本状态机时,就会立即进入出生状态。

@Override
public void enter()
{
    super.enter();
    ScriptFSMManager manager = this.manager;
    manager.getAiScript().enterBirth();
}
游戏工程进入出生状态啥也不做,只是调用enterBirth()接口,
让脚本工程自定义去做什么事(例如:策划在出生时给npc加技能...)

2.tick做的事:

@Override
public void tick(int interval)
{
    super.tick(interval);

    //出生后,立即进入空闲
    manager.switchState(AIScriptStateEnum.IDLE);
}
转换状态,从出生进入空闲。

4.空闲状态ScriptIdleState

1.进入要做的事:

 @Override
public void enter()
{
    super.enter();
    manager.getAiScript().enterIdle();
}
游戏服工程什么也不做,只是调用进入休闲方法enterIdle(),
脚本工程策划在enterIdle()方法里自定义做的事情,例如:
战场,怪物出生后,先找点,到达点的附近,再找配对的目标

2.tick:

@Override
public void tick(int interval)
{
    super.tick(interval);

    NpcAiModule npcAiModule = manager.getNpc().getNpcAiModule();
    DictNpcCommonAiData dictNpcCommonAi = npcAiModule.getDictNpcCommonAi();
    if(dictNpcCommonAi.getOpenIdleAi())
    {
        //开启了空闲AI
        npcAiModule.getIdleAIManager().tick(interval);
    }
    else
    {
        //未开启空闲AI
        ScriptFSMManager manager = this.manager;
        //扫描周围有没有攻击目标
        this.scanInterval -= interval;
        if (dictNpcCommonAi.getChoiseTargetTime() > 0 && this.scanInterval <= 0)
        {
            scanInterval = dictNpcCommonAi.getChoiseTargetTime();
            //扫描敌人最大距离
            int distance = dictNpcCommonAi.getTargetDistance();
            if(distance > 0)
            {
                BPObject bpObject = npcAiModule.hasActor(NpcConstant.NPC_SCAN_SCOPE_CIRCLE, 0, 0, distance, 0, -1, NpcConstant.NPC_SCAN_TARGET_ATTACK);
                if (bpObject != null)
                {
                    Npc npc = manager.getNpc();
                    npc.getEnmityManager().setAttackObjectId(bpObject.getObjectID());
                    //切换到战斗状态
                    manager.switchState(AIScriptStateEnum.BATTLE);
                    //玩家进入某仇恨组怪的警戒范围
                    npc.getScene().getSceneFlowModule().callSceneListener(SceneTriggerTypeEnum.ACTOR_ENTER_NPC_VIEW, bpObject.getObjectID(), npc.getSceneGuid());
                    //呼叫其他组
                    npcAiModule.callOtherGroup(bpObject.getObjectID());

                    return;
                }
            }
        }
        
        //如果是一直在空闲状态,那么调用idleTick()给脚本工程自定义
        manager.getAiScript().idleTick(interval);
    }
}
大体逻辑:
    1.如果脚本怪配置了空闲AI,那么走空闲AI逻辑
    2.未开启:
      扫描周围有没有攻击目标,有则切换到战斗状态。
      没有,一直在空闲状态,那么调用idleTick()给脚本工程自定义

3.接受事件:

 @Override
public void onEvent(AIEventEnum event, int intParam0, int intParam1, String stringParma, Object objectParam)
{
    super.onEvent(event, intParam0, intParam1, stringParma, objectParam);
    switch (event)
    {
        case AI_EVENT_NPC_ALERT:
        case AI_EVENT_NPC_SNEER_START:
        {
            manager.switchState(AIScriptStateEnum.BATTLE);
            break;
        }
        default:
            break;
    }
}
接受到嘲讽、玩家进入目标事件,则切换到战斗状态。

3.exit():

@Override
public void exit()
{
    super.exit();
    //清空扫描间隔参数
    scanInterval = 0;
    //调用exitIdle()方法,给脚本工程自定义
    manager.getAiScript().exitIdle();

    NpcAiModule npcAiModule = manager.getNpc().getNpcAiModule();
    if(npcAiModule.getDictNpcCommonAi().getOpenIdleAi())
    {
        //开启了空闲AI,则需要重置相关数据
        npcAiModule.getIdleAIManager().resetState();
    }
}
1.清空扫描间隔参数
2.调用exitIdle()方法,给脚本工程自定义
3.开启了空闲AI,则需要重置相关数据

5.战斗状态ScriptBattleState(父类)

1.ScriptBattleState是一个父类状态,一进入战斗后,
都是以父类状态驱动子类状态进行操作,相当于一直是ScriptBattleState状态。

2.子类切换状态是有自己单独逻辑:
    /**
     * 子类切换状态
     * @param state 子类型
     */
    public boolean switchSubState(AIScriptStateEnum state)
    {
        //当前子类状态
        AIScriptStateEnum curState = this.subStateEnum;
        if(curState == state)
        {
            return true;
        }
        int index = state.getIndex();
        if(index < 0 || index >= subStates.length)
        {
            BPLog.BP_SYSTEM.error("ParentState switchState error parentState:{} curState:{} targetState:{}",
                    getStateEnum(), stateEnum, state);
            return false;
        }
        //退出当前子状态,注意是子状态!!
        if(subState != null)
        {
            //子状态退出
            subState.exit();
        }
        AbstractScriptSubState subState = subStates[index];
        if(subState != null)
        {
            //子状态进入
            subState.enter();
            this.subState = subState;
            subStateEnum = state;
        }
        else
        {
            //切换为ScriptBattleNoneState
            this.subState = null;
            subStateEnum = AIScriptStateEnum.NONE;
        }
        return true;
    }
    子状态下退出、进入等操作。注意操作的是子状态,都是在父类状态ScriptBattleState
    下驱动完成。

1.脚本战斗状态在new的时候会new很多子状态

 public ScriptBattleState(ScriptFSMManager manager)
{
    super(manager, AIScriptStateEnum.BATTLE);

    ///////////////////////脚本状态状态ScriptBattleState////////
    ///////////////////////添加子状态///////////////////////////
    //战斗下默认状态
    ScriptBattleNoneState noneState = new ScriptBattleNoneState(manager);
    //设置战斗下默认状态d的父类为 脚本战斗状态也就是当前类
    noneState.setParent(this);
    subStates[noneState.getStateEnum().getIndex()] = noneState;
    //强控状态
    ScriptSpecialControlState control = new ScriptSpecialControlState(manager);
    control.setParent(this);
    subStates[control.getStateEnum().getIndex()] = control;
    //强控恢复状态
    ScriptForceControlRecoverState recover = new ScriptForceControlRecoverState(manager);
    recover.setParent(this);
    subStates[recover.getStateEnum().getIndex()] = recover;

    //QTE状态
    ScriptBasaraQteState qteState = new ScriptBasaraQteState(manager);
    qteState.setParent(this);
    subStates[qteState.getStateEnum().getIndex()] = qteState;

    //组策略状态
    ScriptGroupState groupState = new ScriptGroupState(manager);
    groupState.setParent(this);
    subStates[groupState.getStateEnum().getIndex()] = groupState;
}

2.enter():

 @Override
public void enter()
{
    super.enter();
    //切换子状态为ScriptBattleNoneState默认状态
    switchSubState(AIScriptStateEnum.NONE);

    ScriptFSMManager manager = this.manager;
    //切换为战斗姿态
    manager.getNpc().getNpcAiModule().switchBattlePose(true);

    //获取脚本对象,调用进入战斗方法enterBattle(),给策划自定义使用
    AbstractAIScript aiScript = manager.getAiScript();
    aiScript.enterBattle();

    Npc npc = manager.getNpc();
    NpcAiModule aiManager = npc.getNpcAiModule();
    DictNpcCommonAi commonAi = aiManager.getDictNpcCommonAi();
    //是否可以召唤周围怪物, 如果怪物有目标,则返回
    if(commonAi.getIsHelp() == NpcConstant.NPC_CAN_HELP)
    {
        NpcAlertState.getInstance().setAttackTarget(npc);
    }
    //检测是否在水中倒计时
    aiManager.initCheckWaterInterval();
}
1.切换子状态为ScriptBattleNoneState默认状态
2.切换为战斗姿态
3.判断是否可以召唤周围怪物
4.检测是否在水中倒计时

3.exit():

@Override
public void exit()
{
    super.exit();
    manager.getAiScript().exitBattle();
}
游戏工程,不做什么,调用退出战斗状态exitBattle()方法,给
脚本工程策划自定义使用。

6.战斗下默认状态ScriptBattleNoneState(子类)

1.tick:

@Override
public void tick(int interval)
{
    super.tick(interval);
    ScriptFSMManager manager = this.manager;

    NpcAiModule npcAiModule = manager.getNpc().getNpcAiModule();
    //如果在水中,则直接返回
    if (npcAiModule.checkInWater(interval))
    {
        manager.switchState(AIScriptStateEnum.RESET);
        return;
    }

    Npc npc = manager.getNpc();
    //脱战距离判断
    DictNpcCommonAiData dictNpcCommonAi = npcAiModule.getDictNpcCommonAi();
    int distance = dictNpcCommonAi.getBirthDistance();
    int pos = MapUtils.distanceSumBetweenPos(npc.getBirthX(), npc.getBirthZ(), npc.getX(), npc.getZ());
    if(pos >= distance)
    {
        //如果脱战,调用的是状态机下切换状态,会从战斗父状态直接退出到复位状态.
        manager.switchState(AIScriptStateEnum.RESET);
        return;
    }
    
    //战斗状态下,是否进行目标检测
    //目标丢失了,则切回复位状态
    if(dictNpcCommonAi.getBattleCheckTarget())
    {
        EnmityManager enmityManager = npc.getEnmityManager();
        AbstractCharacter target = npc.getCharacter(enmityManager.getAttackObjectId());
        if (target != null)
        {
            //检测距离目标的距离
            if (enmityManager.checkNpcToTargetDis(target.getX(), target.getZ()))
            {
                manager.switchState(AIScriptStateEnum.RESET);
                return;
            }
        }

        //查找是否有新目标
        target = enmityManager.checkTargetHatredAndScan();
        if (target == null)
        {
            manager.switchState(AIScriptStateEnum.RESET);
            return;
        }
    }

    AbstractAIScript aiScript = manager.getAiScript();
    if(aiScript != null)
    {
        //战斗状态持续battleTick(),给脚本工程策划使用自定义
        aiScript.battleTick(interval);
    }
}

子状态没有退出方法,退出状态都默认调用父战斗状态的退出。

7.战斗下 basara QTE 状态ScriptBasaraQteState(子类)

1.enter:

@Override
public void enter()
{
    super.enter();
    ScriptFSMManager manager = this.manager;
    //锁定攻击目标, tick不检查切换目标
    //解除锁定时,再次检索仇恨值
    manager.getNpc().getEnmityManager().lockTargetId(true);

    //驱动脚本工程
    manager.getAiScript().enterBasaraQte();
}

2.tick:

@Override
public void tick(int interval)
{
    super.tick(interval);

    //驱动脚本
    manager.getAiScript().basaraQteTick(interval);
}

3.exit:

@Override
public void exit()
{
    super.exit();
    //取消锁定目标
    ScriptFSMManager manager = this.manager;
    Npc npc = manager.getNpc();
    npc.getEnmityManager().lockTargetId(false);

    //结束QTE
    npc.getBasaraQteModule().manualEndQte();

    //驱动脚本
    manager.getAiScript().exitBasaraQte();
}

8.脱战(重置)状态ScriptResetState

1.enter:

1.不是默认战斗姿态,则通知前端,取消战斗待机
2.调用主动脱战
3.满霸体
4.设置强控为false
5.调用enterReset()脱战方法给脚本工程策划自定义
    manager.getAiScript().enterReset();
6.通知场景事件进行脱战

2.tick:

1.判断是否到达出生点:
   重置为出生朝向,广播状态变化
   切换空闲状态
2.每到出生点,寻路

3.退出:

1.停止移动
2.消耗仇恨管理器

3.总结:

脚本复位状态和正常ai复位状态极其类似,这些逻辑其实可以考虑抽象一个静态类,统一
调用以便重复利用。

6.扩展AI

1.初始化扩展AI

/** 初始化正常ai */
private void normalAiInit()
{
    // 初始化拓展AI
    initExtraAi(owner);
}
//初始化逻辑数据
//	 * 扩展ai条件数据(已经根据优先级排序,后面的优先级高)
//	 * 0:已选点释放技能 1:未选点释放技能
DictExtraAiLogic[][] dictLogics = dictExtraAiData.getLogicDatas();
int size1 = dictLogics.length;

int size2;
AbstractExtraAiLogic[][] allLogics = new AbstractExtraAiLogic[size1][];
for (int i = 0; i < size1; i++)
{
    DictExtraAiLogic[] logicDatas = dictLogics[i];
    size2 = logicDatas.length;
    allLogics[i] = new AbstractExtraAiLogic[size2];
    for (int j = 0; j < size2; j++)
    {
        DictExtraAiLogic logicData = logicDatas[j];
        if(logicData == null)
        {
            continue;
        }
        //创建扩展逻辑
        AbstractExtraAiLogic extraAiLogic = ExtraAiLogicFactory.getExtraAiLogic(owner, logicData);
        allLogics[i][j] = extraAiLogic;

        //是否是第一次需要释放的类型
        if(ExtraAiLogicTypeEnum.isFirstUse(logicData.getConditionId()))
        {
            owner.getNpcAiModule().setFirstUseSkill(true);
        }
    }
}

this.allLogics = allLogics;

//将用到技能,设置到已学习技能map中
TIntIterator iterator = getSkillIds().iterator();
while(iterator.hasNext())
{
    int skillId = iterator.next();

    DictSkillGroup skillGroup = DictSkillGroupData.getRecordById(skillId);
    if(skillGroup == null)
    {
        BPLog.BP_SCENE.warn("【通用AI错误】技能找不到对应配置 extraAI skill error [dictExtraAi-Id] {}  [skilId] {}", dictExtraAiData.getId(), skillId);
        continue;
    }
    owner.getSkillModule().defaultStateUnlockSkill(skillId,false);
}

创建扩展逻辑(其实就是技能逻辑),npc学习技能。

2.什么时候开始tick扩展ai逻辑

NpcAiModule:

/** 正常AI tick */
private void normalAiTick(int tickInterval)
{
    tickExtraLogic(tickInterval);
}

/**
* 只在战斗下, 更新所有逻辑的tick
* (所有逻辑的tick时间增加)
*/
public void tickExtraLogic(int interval)
{
    //如果是空闲状态直接返回
    if(isIdle)
    {
        return;
    }
    extraAiLogic.tick(interval);
}

只在战斗下tick驱动扩展ai,主要就是放技能。

3.类成员变量

/**
 * 扩展逻辑
 */
public abstract class AbstractExtraAiLogic<T extends AbstractCharacter>
{
    protected T owner;

    /**
     * 心跳tick
     * @param interval
     */
    public final void tick(int interval)
    {
        toTick(interval);
    }

    /**
     * 子类可以复写的方法
     * @param interval
     */
    protected abstract void toTick(int interval);

      /**
     * 检查相关逻辑
     *
     * 检查是否有可释放技能;
     * 检查相关逻辑
     * @return 是否需要可切换到战斗状态
     */
    public final boolean checkExtraLogic()
    {

    }

     /**
     * 子类覆写,检测是否有技能释放
     * @return
     */
    protected boolean toCheckExtraLogic()
    {
        return true;
    }

    /**
     * 相关事件触发
     */
    public void onEvent(LogicEventTypeEnum event, int param0, int param1, int param2)
    {
    }
}
主要:
    方法:
       1.tick
       2.checkExtraLogic()检测是否有可放技能
       3.onEvent()事件触发

Search

    Table of Contents