407 lines
16 KiB
C#
407 lines
16 KiB
C#
/*
|
|
* @Author: 白哉
|
|
* @Description: AI 逻辑总模块
|
|
* @Date: 2025年04月01日 星期二 14:04:01
|
|
* @Modify:
|
|
*/
|
|
|
|
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using Logic.Action;
|
|
using Logic.CrashSight;
|
|
using NodeCanvas.BehaviourTrees;
|
|
using NodeCanvas.Framework;
|
|
using UnityEngine;
|
|
using RuntimeData;
|
|
using TH1_Core.Managers;
|
|
using TH1_Logic.AITrain;
|
|
using Unity.VisualScripting;
|
|
|
|
|
|
namespace Logic.AI
|
|
{
|
|
public enum AILogicState
|
|
{
|
|
Prepare,
|
|
Playing,
|
|
PrePlay,
|
|
Pausing,
|
|
Finished,
|
|
}
|
|
|
|
|
|
public enum AIActionType
|
|
{
|
|
Grid,
|
|
City,
|
|
Unit,
|
|
Tech,
|
|
Max,
|
|
}
|
|
|
|
|
|
public class AILogic
|
|
{
|
|
public AILogicState AILogicState;
|
|
public PlayerData PlayerData => _playerData;
|
|
|
|
private float _targetTime;
|
|
private bool _isWaitFrame;
|
|
private AIActionScoreCalculator _scoreCalculator;
|
|
private AIActionGenerator _generator;
|
|
|
|
private List<AIActionBase> RecordActions;
|
|
private AIActionBase MaxScoreAction;
|
|
|
|
private MapData _mapData;
|
|
private PlayerData _playerData;
|
|
private AIConfigAsset _cfg;
|
|
|
|
private GameObject _logicObject;
|
|
private BehaviourTreeOwner _btOwner;
|
|
private AICalculatorData _data;
|
|
private int _actionCount;
|
|
private List<float> _actionBitCodec;
|
|
private int _sameCount;
|
|
|
|
public static uint CurrentAIPlayerId;
|
|
public static Dictionary<uint, List<AIRecord>> AIRecordsDict;
|
|
|
|
|
|
|
|
public AILogic()
|
|
{
|
|
AIRecordsDict = new Dictionary<uint, List<AIRecord>>();
|
|
AILogicState = AILogicState.Prepare;
|
|
RecordActions = new List<AIActionBase>();
|
|
_scoreCalculator = new AIActionScoreCalculator();
|
|
_cfg = Resources.Load<AIConfigAsset>("Export/AIConfig");
|
|
_generator = new AIActionGenerator();
|
|
|
|
_logicObject = GameObject.Find("AIBT");
|
|
_btOwner = _logicObject.GetComponent<BehaviourTreeOwner>();
|
|
var data = _btOwner.blackboard.GetVariable<AICalculatorData>("Data");
|
|
if (data.value == null)
|
|
{
|
|
_data = new AICalculatorData();
|
|
data.value = _data;
|
|
}
|
|
else _data = data.value;
|
|
}
|
|
|
|
// 开始 AI 逻辑
|
|
public void StartAILogic(MapData mapData, PlayerData playerData)
|
|
{
|
|
AILogicState = AILogicState.Playing;
|
|
_actionCount = 0;
|
|
_mapData = mapData;
|
|
_playerData = playerData;
|
|
var aiDiff = _cfg.GetAIDiffInfo(_mapData.MapConfig.AIDiff);
|
|
_data.AiDiffInfo = aiDiff;
|
|
_generator.Init(_mapData, _playerData);
|
|
_data.Refresh(mapData, playerData);
|
|
_btOwner.StopBehaviour();
|
|
_btOwner.StartBehaviour();
|
|
MainEditor.Instance.Data = _data;
|
|
|
|
#if UNITY_EDITOR
|
|
CurrentAIPlayerId = _playerData.Id;
|
|
if (!AIRecordsDict.ContainsKey(CurrentAIPlayerId)) AIRecordsDict[CurrentAIPlayerId] = new List<AIRecord>();
|
|
AIRecordsDict[CurrentAIPlayerId].Clear();
|
|
#endif
|
|
}
|
|
|
|
// 结束 AI 逻辑
|
|
public void FinishAILogic()
|
|
{
|
|
_playerData = null;
|
|
AILogicState = AILogicState.Prepare;
|
|
}
|
|
|
|
// 更新 AI 逻辑
|
|
public void Update()
|
|
{
|
|
if (AILogicState == AILogicState.Finished || AILogicState == AILogicState.Prepare) return;
|
|
|
|
#if ENABLE_SPEEDUP
|
|
if (AILogicState == AILogicState.Pausing)
|
|
#else
|
|
if (AILogicState == AILogicState.Pausing && !PresentationManager.Busy)
|
|
#endif
|
|
{
|
|
_targetTime -= Time.deltaTime;
|
|
if (_targetTime <= 0) AILogicState = AILogicState.Playing;
|
|
// if (!_isWaitFrame) _isWaitFrame = true;
|
|
// else
|
|
// {
|
|
// _targetTime -= Time.deltaTime;
|
|
// if (_targetTime <= 0) AILogicState = AILogicState.Playing;
|
|
// }
|
|
}
|
|
|
|
if (AILogicState == AILogicState.Playing)
|
|
{
|
|
if (_actionCount > 200)
|
|
{
|
|
LogSystem.LogError($"AI 行为次数过多,可能进入死循环,强制结束 AI 逻辑 最终记录点为:{MainEditor.Instance.BTNodeId}");
|
|
AILogicState = AILogicState.Finished;
|
|
return;
|
|
}
|
|
|
|
#if ENABLE_AIMODEL
|
|
AIModelExecute();
|
|
return;
|
|
#endif
|
|
|
|
var index = 0;
|
|
var nodeRecords = new List<List<uint>>();
|
|
while (true)
|
|
{
|
|
if (MainEditor.Instance.IsEditor && !MainEditor.Instance.IsGo) return;
|
|
index++;
|
|
if (index > nodeRecords.Count) nodeRecords.Add(new List<uint>());
|
|
_data.ClearCache();
|
|
nodeRecords[index - 1].Add(MainEditor.Instance.BTNodeId);
|
|
_btOwner.UpdateBehaviour();
|
|
MainEditor.Instance.IsGo = false;
|
|
nodeRecords[index - 1].Add(MainEditor.Instance.BTNodeId);
|
|
if (_data.MaxAiAction != null || _data.IsFinish) break;
|
|
|
|
if (index > 150)
|
|
{
|
|
LogSystem.LogError($"死循环了,最终记录点为:{MainEditor.Instance.BTNodeId}");
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (_data.MaxAiAction == null || index > 100) AILogicState = AILogicState.Finished;
|
|
else
|
|
{
|
|
|
|
#if ENABLE_TRAIN
|
|
bool isPack = TrainingState.Instance.GetActionBitCodec(_data.MaxAiAction.ActionLogic.ActionId, _data.MaxAiAction.Param, out var packed);
|
|
var curPlayer = _data.MaxAiAction.Param.MapData.CurPlayer;
|
|
var beforeScore = TrainingState.Instance.GetMapScore(_data.MaxAiAction.Param.MapData, curPlayer);
|
|
var state = TrainingState.Instance.GetMapState(_data.MaxAiAction.Param.MapData, curPlayer);
|
|
var validActions = TrainingState.Instance.GetAllActionBitCodec(_data.MaxAiAction.Param.MapData, curPlayer, out var actions);
|
|
if (isPack)
|
|
{
|
|
bool found = false;
|
|
|
|
foreach (var action in validActions)
|
|
{
|
|
if (action.Count != packed.Count) continue;
|
|
for (int i = 0; i < packed.Count; i++)
|
|
{
|
|
if (action[i] - packed[i] > 0.001f) break;
|
|
if (i == packed.Count - 1) found = true;
|
|
}
|
|
}
|
|
|
|
if (!found)
|
|
{
|
|
validActions.Add(packed);
|
|
LogSystem.LogError($"训练数据出错: {_data.MaxAiAction.ActionLogic.ActionId.GetStringLog()}");
|
|
}
|
|
}
|
|
#endif
|
|
|
|
TrainingState.Instance.GetActionBitCodecWithoutLimit(_data.MaxAiAction.ActionLogic.ActionId, _data.MaxAiAction.Param, out var packed);
|
|
if (_actionBitCodec != null && _actionBitCodec.Count == packed.Count)
|
|
{
|
|
bool isSame = true;
|
|
for (int i = 0; i < packed.Count; i++)
|
|
{
|
|
if (Mathf.Approximately(packed[i], _actionBitCodec[i])) continue;
|
|
isSame = false;
|
|
break;
|
|
}
|
|
|
|
if (isSame)
|
|
{
|
|
_sameCount++;
|
|
LogSystem.LogError($"存在相似action ,记录点为:{MainEditor.Instance.BTNodeId} ," +
|
|
$"Action为:{_data.MaxAiAction.ActionLogic.ActionId.GetStringLog()} " +
|
|
$"重复次数 :{_sameCount}");
|
|
}
|
|
else
|
|
{
|
|
_sameCount = 0;
|
|
}
|
|
}
|
|
|
|
_data.MaxAiAction.ActionLogic.CompleteExecute(_data.MaxAiAction.Param);
|
|
_actionBitCodec = packed;
|
|
_actionCount++;
|
|
|
|
#if ENABLE_TRAIN
|
|
if (isPack)
|
|
{
|
|
var afterScore = TrainingState.Instance.GetMapScore(_data.MaxAiAction.Param.MapData, curPlayer);
|
|
var reward = afterScore - beforeScore;
|
|
if (_data.MaxAiAction.ActionLogic.ActionId.UnitActionType == UnitActionType.Capture)
|
|
{
|
|
reward += 10;
|
|
}
|
|
if (_data.MaxAiAction.ActionLogic.ActionId.UnitActionType == UnitActionType.Upgrade)
|
|
{
|
|
reward += 10;
|
|
}
|
|
if (_data.MaxAiAction.ActionLogic.ActionId.UnitActionType == UnitActionType.HeroUpgrade)
|
|
{
|
|
reward += 10;
|
|
}
|
|
if (_data.MaxAiAction.ActionLogic.ActionId.UnitActionType == UnitActionType.Gather)
|
|
{
|
|
reward += 10;
|
|
}
|
|
if (_data.MaxAiAction.ActionLogic.ActionId.UnitActionType == UnitActionType.Examine)
|
|
{
|
|
reward += 10;
|
|
}
|
|
TrainingDataRecorder.Instance.RecordStep(curPlayer.Id, state, validActions.Select(x => x.ToArray()).ToArray(), packed.ToArray(), reward);
|
|
}
|
|
#endif
|
|
|
|
_data.MaxAiAction.CheckIsActionDuration();
|
|
_targetTime = Mathf.Max(_data.MaxAiAction.Duration, 0f);
|
|
|
|
#if UNITY_EDITOR
|
|
var records = GetCurrentAIRecords();
|
|
if (records != null && records.Count != 0)
|
|
{
|
|
var record = records[^1];
|
|
record.Action = _data.MaxAiAction;
|
|
}
|
|
#endif
|
|
|
|
_isWaitFrame = false;
|
|
MainEditor.Instance.OnActionExcuted();
|
|
_data.MaxAiAction = null;
|
|
AILogicState = AILogicState.Pausing;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void AIModelExecute()
|
|
{
|
|
#if ENABLE_AIMODEL
|
|
var predictState = TrainingState.Instance.GetMapState(_data.Map, _data.Map.CurPlayer);
|
|
var predictActionsId = TrainingState.Instance.GetAllActionBitCodecForUse(_data.Map, _data.Map.CurPlayer, out var predictActions);
|
|
if (predictActionsId.Count == 0)
|
|
{
|
|
AILogicState = AILogicState.Finished;
|
|
}
|
|
else
|
|
{
|
|
var predictIndex = ModelInference.Instance.Predict(predictState, predictActionsId);
|
|
// for (int i = 0; i < predictActions.Count; i++)
|
|
// {
|
|
// if (predictActions[i].ActionLogic.ActionId.UnitActionType == UnitActionType.Capture)
|
|
// predictIndex = i;
|
|
// }
|
|
if (!TrainingState.Instance.GetActionFromBitCodec(predictActionsId[predictIndex], _data.Map, out var actionId, out var param))
|
|
{
|
|
LogSystem.LogError($"反序列化 Action 失败");
|
|
}
|
|
else
|
|
{
|
|
_data.MaxAiAction = new AIActionBase(param, ActionLogicFactory.GetActionLogic(actionId));
|
|
|
|
#if ENABLE_TRAIN
|
|
bool isPack = TrainingState.Instance.GetActionBitCodec(_data.MaxAiAction.ActionLogic.ActionId, _data.MaxAiAction.Param, out var packed);
|
|
var curPlayer = _data.MaxAiAction.Param.MapData.CurPlayer;
|
|
var beforeScore = TrainingState.Instance.GetMapScore(_data.MaxAiAction.Param.MapData, curPlayer);
|
|
var state = TrainingState.Instance.GetMapState(_data.MaxAiAction.Param.MapData, curPlayer);
|
|
var validActions = TrainingState.Instance.GetAllActionBitCodec(_data.MaxAiAction.Param.MapData, curPlayer, out var actions);
|
|
if (isPack)
|
|
{
|
|
bool found = false;
|
|
|
|
foreach (var action in validActions)
|
|
{
|
|
if (action.Count != packed.Count) continue;
|
|
for (int i = 0; i < packed.Count; i++)
|
|
{
|
|
if (action[i] - packed[i] > 0.001f) break;
|
|
if (i == packed.Count - 1) found = true;
|
|
}
|
|
}
|
|
|
|
if (!found)
|
|
{
|
|
validActions.Add(packed);
|
|
LogSystem.LogError($"训练数据出错: {_data.MaxAiAction.ActionLogic.ActionId.GetStringLog()}");
|
|
}
|
|
}
|
|
#endif
|
|
|
|
_data.MaxAiAction.ActionLogic.CompleteExecute(_data.MaxAiAction.Param);
|
|
|
|
#if ENABLE_TRAIN
|
|
if (isPack)
|
|
{
|
|
var afterScore = TrainingState.Instance.GetMapScore(_data.MaxAiAction.Param.MapData, curPlayer);
|
|
var reward = afterScore - beforeScore;
|
|
if (_data.MaxAiAction.ActionLogic.ActionId.UnitActionType == UnitActionType.Capture)
|
|
{
|
|
LogSystem.LogError($"占领!!!");
|
|
reward += 10;
|
|
}
|
|
TrainingDataRecorder.Instance.RecordStep(curPlayer.Id, state, validActions.Select(x => x.ToArray()).ToArray(), packed.ToArray(), reward);
|
|
}
|
|
#endif
|
|
|
|
LogSystem.LogWarning($"{_data.MaxAiAction.ActionLogic.ActionId.GetStringLog()}");
|
|
_data.MaxAiAction.CheckIsActionInPlayerSight();
|
|
_data.MaxAiAction.CheckIsActionDuration();
|
|
_targetTime = Mathf.Max(_data.MaxAiAction.Duration, 0f);
|
|
_isWaitFrame = false;
|
|
_data.MaxAiAction = null;
|
|
AILogicState = AILogicState.Pausing;
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
public static List<AIRecord> GetCurrentAIRecords()
|
|
{
|
|
return AIRecordsDict.GetValueOrDefault(CurrentAIPlayerId);
|
|
}
|
|
|
|
public static List<AIRecord> GetAIRecords(uint playerId)
|
|
{
|
|
return AIRecordsDict.GetValueOrDefault(playerId);
|
|
}
|
|
}
|
|
|
|
|
|
public class AIStateRecord
|
|
{
|
|
public uint ID;
|
|
public string Desc;
|
|
public bool Result;
|
|
}
|
|
|
|
|
|
public class AIRecord
|
|
{
|
|
public List<AIStateRecord> StateRecords;
|
|
public AIActionBase Action;
|
|
public bool IsFoldout;
|
|
|
|
|
|
public AIRecord()
|
|
{
|
|
IsFoldout = false;
|
|
StateRecords = new List<AIStateRecord>();
|
|
}
|
|
|
|
public string GetDesc()
|
|
{
|
|
if (Action == null) return "暂无动作";
|
|
return Action.ActionLogic.GetType().ToString();
|
|
}
|
|
}
|
|
} |