【关卡编辑器】可视化编辑NPC

main
zhangaotian 2023-11-16 15:57:47 +08:00
parent 4294319b28
commit 15f231cef6
3 changed files with 209 additions and 33 deletions

View File

@ -1,4 +1,6 @@
namespace Framework
using NPOI.SS.Formula.Functions;
namespace Framework
{
public partial class Constants
{
@ -14,11 +16,13 @@
public const string ANIMATOR_CONFIG_FORMAT_PATH = "Assets/Config/Skills/AnimatorInfos/animator_{0}.json";
public const string OUTLINE_POSTPROCESS_PATH = "Assets/Level/Prefabs/OutlineManager.prefab";
public const string MAP_STRUCTURE_PATH_WITHOUT_ASSET = "Art/Character/Prefabs/Structure";
public const string LEVEL_NPC_PATH_WITHOUT_ASSET = "Art/Character/Prefabs/Enemy";
public const string CHARACTER_SELECT_GROUND_GFX = "Assets/Art/Gfx/Prefabs/GfxSelectedGround.prefab";
public const string CHARACTER_TARGET_GFX = "Assets/_Test/Prefab/EndNotice.prefab";
public const string TIREMARK_UNIT_PATH = "Assets/Art/Scene/TireMark/tiremark_panel.prefab";
public const string TIREMARK_MAT_PATH = "Assets/Art/Scene/TireMark/mat_tiremark_draw.mat";
public const string LEVEL_NPC_PATH = "Assets/Art/Character/Prefabs/Enemy/{0}.prefab";
public const string EMOJI_HEAD_MAT = "Assets/Art/Character/Models/Emoji/Mat/M_emoji_head.mat";

View File

@ -68,8 +68,10 @@ namespace Gameplay.Level
[Serializable]
public class NPCInfo
{
[Sirenix.OdinInspector.ReadOnly]
public int id;
public int level;
[Sirenix.OdinInspector.ReadOnly]
public Vector2Int position;
[LabelText("AI类型")]
public AIType aIType;
@ -133,15 +135,19 @@ namespace Gameplay.Level
public string mapName;
[LabelText("出战区")]
[HideInInspector]
public int preparingRegionId;
[LabelText("出战区朝向")]
[HideInInspector]
public CharacterToward preparingRegionToward;
[LabelText("撤退区")]
[HideInInspector]
public int fallbackRegionId;
[LabelText("敌方撤退区")]
[HideInInspector]
public int enemyFallbackRegionId;
[LabelText("天气")]
@ -157,8 +163,9 @@ namespace Gameplay.Level
[ListDrawerSettings(HideAddButton = true,HideRemoveButton = true)]
public List<ConditionModifier> starTarget = new List<ConditionModifier>(){new ConditionModifier(ConditionUtil.LevelStarConditionFilter),new ConditionModifier(ConditionUtil.LevelStarConditionFilter),new ConditionModifier(ConditionUtil.LevelStarConditionFilter)};
[InfoBox("敌人默认朝向为六边形TopLeft以游戏视角的六边形为模板方向")]
[LabelText("敌人")]
/*[InfoBox("敌人默认朝向为六边形TopLeft以游戏视角的六边形为模板方向")]
[LabelText("敌人")] */
[HideInInspector]
public List<NPCInfo> npcInfos = new List<NPCInfo>(16);
[OnDeserialized]
@ -169,5 +176,7 @@ namespace Gameplay.Level
starCondition.SetFilterTypes(ConditionUtil.LevelStarConditionFilter);
}
}
}
}

View File

@ -8,20 +8,24 @@ using Sirenix.Utilities;
using Sirenix.Utilities.Editor;
using System;
using System.Collections.Generic;
using System.Linq;
using TGS;
using UnityEditor;
using UnityEngine;
using UnityEngine.Serialization;
using Constants = Framework.Constants;
using ObjectFieldAlignment = Sirenix.OdinInspector.ObjectFieldAlignment;
public class LevelEditorWindow : OdinEditorWindow
{
private static readonly Color regionColor = MapUtils.GetRGBAColor("FF960072");
[Serializable]
public class LevelPresentation
{
[ReadOnly] public string levelId = "temp";
[ReadOnly]
public string levelId = "temp";
[Button("编辑"), VerticalGroup("")]
public void Open()
@ -34,7 +38,7 @@ public class LevelEditorWindow : OdinEditorWindow
thisWindow.SaveCurrentLevel();
thisWindow.LoadLevel(levelId);
currentLevelPresentation = this;
} ,
},
() =>
{
thisWindow.LoadLevel(levelId);
@ -50,34 +54,87 @@ public class LevelEditorWindow : OdinEditorWindow
}
}
[Serializable]
public class RegionData
{
[LabelText("出战区")]
[OnValueChanged("OnRegionChanged")]
public int preparingRegionId;
[LabelText("出战区朝向")]
public LevelData.CharacterToward preparingRegionToward;
[LabelText("撤退区")]
public int fallbackRegionId;
[LabelText("敌方撤退区")]
public int enemyFallbackRegionId;
private void OnRegionChanged()
{
var regionslist = thisWindow._curMap.MapData.GetAllRegions();
for(int i = 0; i < regionslist.Count; i++)
{
thisWindow._curMap.SetRegionColor(i, false, regionColor);
}
thisWindow._curMap.SetRegionColor(preparingRegionId, true, regionColor);
}
}
private static LevelPresentation currentLevelPresentation;
private Dictionary<Vector2Int, GameObject> _npcDic;
private Map _curMap;
private static LevelEditorWindow thisWindow;
[TitleGroup("关卡编辑器")]
[HorizontalGroup("关卡编辑器/Split", Width = 0.4f)]
[TableList(ShowIndexLabels = true, AlwaysExpanded = true, DrawScrollView = true, ScrollViewHeight = 500), LabelText("所有关卡")]
[TableList(ShowIndexLabels = true, AlwaysExpanded = true, DrawScrollView = true, ScrollViewHeight = 500),
LabelText("所有关卡")]
[OnValueChanged("OnLevelListChanged")]
public List<LevelPresentation> LevelList = null;
[VerticalGroup("关卡编辑器/Split/Right")]
[VerticalGroup("关卡编辑器/Split/Right")]
[LabelText("关卡名称"), Indent]
public string levelName;
[VerticalGroup("关卡编辑器/Split/Right")]
[TextArea, LabelText("关卡描述"),Indent]
[VerticalGroup("关卡编辑器/Split/Right")]
[TextArea, LabelText("关卡描述"), Indent]
public string levelDesc;
[VerticalGroup("关卡编辑器/Split/Right")]
[Indent]
[LabelText("当前关卡数据")]
[VerticalGroup("关卡编辑器/Split/Right")]
[LabelText("当前关卡数据"),Indent]
[OnValueChanged("OnCurrentLevelDataChanged")]
public LevelData currentLevelData = null;
[VerticalGroup("关卡编辑器/Split/Right")]
[LabelText("当前地区数据"),Indent]
[OnValueChanged("OnCurrentLevelDataChanged")]
public RegionData curRegionData = null;
[VerticalGroup("关卡编辑器/Split/Right")]
[LabelText("添加NPC")]
[LabelWidth(100)]
[SerializeField]
public bool canAddNpc;
[VerticalGroup("关卡编辑器/Split/Right")]
[PreviewField(150,ObjectFieldAlignment.Center)]
[AssetList(Path = (Constants.LEVEL_NPC_PATH_WITHOUT_ASSET))]
[ShowIf("canAddNpc")]
[LabelText("选择NPC")]
public GameObject npcObject;
[VerticalGroup("关卡编辑器/Split/Right")] [LabelText("当前NPC数据")]
public List<LevelData.NPCInfo> CurrentNpcInfo = null;
[VerticalGroup("关卡编辑器/Split/Right")]
[Indent]
[EnableIf("@this.mapPrefab != null")]
@ -86,15 +143,16 @@ public class LevelEditorWindow : OdinEditorWindow
{
currentLevelData.mapName = mapPrefab.name;
EditorMap.Instance.Current = await MapEditorUtils.LoadMap(currentLevelData.mapName);
_curMap = EditorMap.Instance.Current;
}
[VerticalGroup("关卡编辑器/Split/Right")]
[Indent]
[AssetList(Path = (Constants.MAP_ASSET_PATH_WITHOUT_ASSET))]
[PreviewField(200, ObjectFieldAlignment.Center)]
public GameObject mapPrefab = null;
[VerticalGroup("关卡编辑器/Split/Right")]
[Indent]
[Button("保存当前关卡")]
@ -102,12 +160,19 @@ public class LevelEditorWindow : OdinEditorWindow
{
CheckCorrectLevel();
var configFilePath = string.Format(Constants.LEVEL_CONFIG_FORMAT_PATH, currentLevelData.id);
// VersionControlSystem.Checkout(configFilePath);
currentLevelData.preparingRegionId = curRegionData.preparingRegionId;
currentLevelData.fallbackRegionId = curRegionData.fallbackRegionId;
currentLevelData.enemyFallbackRegionId = curRegionData.enemyFallbackRegionId;
currentLevelData.preparingRegionToward = curRegionData.preparingRegionToward;
//NPC列表
currentLevelData.npcInfos = CurrentNpcInfo;
JsonHelper.SaveJson(configFilePath, currentLevelData);
UpdateLevelList(currentLevelData);
CustomPopUpWindow.PopUp($"保存关卡数据成功:{configFilePath}");
}
[MenuItem("Tools/LevelEditor")]
private static void OpenWindow()
@ -126,41 +191,46 @@ public class LevelEditorWindow : OdinEditorWindow
private void CheckCorrectLevel()
{
var map = EditorMap.Instance.Current;
if (map == null)
if (_curMap == null)
{
return;
}
var regionList = map.MapData.GetAllRegions();
var regionList = _curMap.MapData.GetAllRegions();
var errorMessage = string.Empty;
if (!regionList.ContainsKey(currentLevelData.preparingRegionId))
{
errorMessage += string.Format(preparingRegionErrorMessage, currentLevelData.mapName, currentLevelData.preparingRegionId);
errorMessage += string.Format(preparingRegionErrorMessage, currentLevelData.mapName,
currentLevelData.preparingRegionId);
currentLevelData.preparingRegionId = 0;
}
if (!regionList.ContainsKey(currentLevelData.fallbackRegionId))
{
errorMessage += string.Format(fallbackRegionErrorMessage, currentLevelData.mapName, currentLevelData.fallbackRegionId);
errorMessage += string.Format(fallbackRegionErrorMessage, currentLevelData.mapName,
currentLevelData.fallbackRegionId);
currentLevelData.fallbackRegionId = 0;
}
if (!regionList.ContainsKey(currentLevelData.enemyFallbackRegionId))
{
errorMessage += string.Format(enemyFallbackRegionErrorMessage, currentLevelData.mapName, currentLevelData.enemyFallbackRegionId);
errorMessage += string.Format(enemyFallbackRegionErrorMessage, currentLevelData.mapName,
currentLevelData.enemyFallbackRegionId);
currentLevelData.enemyFallbackRegionId = 0;
}
if(!string.IsNullOrEmpty(errorMessage))
if (!string.IsNullOrEmpty(errorMessage))
{
CustomPopUpWindow.PopUp(errorMessage + fixedMessage);
}
}
protected override void OnEnable()
{
TerrainTypeConfig.Load();
_npcDic = new Dictionary<Vector2Int, GameObject>();
base.OnEnable();
}
@ -169,6 +239,8 @@ public class LevelEditorWindow : OdinEditorWindow
thisWindow = null;
MapEditorUtils.UnLoadMap(EditorMap.Instance);
DestroyImmediate(GameObject.Find("Main Camera"));
DestroyNpcPrefab();
_npcDic = null;
base.OnDisable();
}
@ -187,6 +259,7 @@ public class LevelEditorWindow : OdinEditorWindow
}
private bool banSaveLevelList = false;
private void OnLevelListChanged()
{
if (!banSaveLevelList)
@ -199,21 +272,23 @@ public class LevelEditorWindow : OdinEditorWindow
Debug.Log($"{GetType()}.New Level declared : {levelPresentation.levelId}");
}
}
SaveLevelList();
Debug.Log($"{GetType()}.Save Level List");
banSaveLevelList = false;
}
}
private void OnCurrentLevelDataChanged()
{
Debug.Log($"{GetType()}.OnCurrentLevelDataChanged");
}
private void SaveLevelList()
{
var configFilePath = string.Format(Constants.LEVEL_LIST_PATH);
// VersionControlSystem.Checkout(configFilePath);
JsonHelper.SaveJson(configFilePath, LevelList);
}
@ -222,21 +297,109 @@ public class LevelEditorWindow : OdinEditorWindow
private async void LoadLevel(string id)
{
DestroyNpcPrefab();
_npcDic.Clear();
var configFilePath = string.Format(Constants.LEVEL_CONFIG_FORMAT_PATH, id);
currentLevelData = JsonHelper.LoadJson<LevelData>(configFilePath);
//加载地区信息
curRegionData = new RegionData();
curRegionData.preparingRegionId = currentLevelData.preparingRegionId;
curRegionData.fallbackRegionId = currentLevelData.fallbackRegionId;
curRegionData.enemyFallbackRegionId = currentLevelData.enemyFallbackRegionId;
curRegionData.preparingRegionToward = currentLevelData.preparingRegionToward;
if (!string.IsNullOrEmpty(currentLevelData.mapName))
{
EditorMap.Instance.Current = await MapEditorUtils.LoadMap(currentLevelData.mapName);
_curMap = EditorMap.Instance.Current;
// 显示在关卡编辑器中的地图资产
mapPrefab = await MapUtils.LoadMapAsset(currentLevelData.mapName);
_curMap.TerrainGridSystem.OnMouseClickOnGrid += OnCellClick;
}
else
{
MapEditorUtils.UnLoadMap(EditorMap.Instance);
mapPrefab = null;
}
//加载NPC到场景中
CurrentNpcInfo = new List<LevelData.NPCInfo>();
foreach (var npcInfo in currentLevelData.npcInfos)
{
CurrentNpcInfo.Add(npcInfo);
string npcId = string.Format("P_E{0}", npcInfo.id.ToString().Substring(1));
var npc = AssetDatabase.LoadAssetAtPath<GameObject>(string.Format(Constants.LEVEL_NPC_PATH, npcId));
var position = _curMap.BlockPosition2WorldPosition(npcInfo.position);
_npcDic.Add(npcInfo.position, Instantiate(npc, position, npc.transform.rotation));
}
}
private void OnCellClick(TerrainGridSystem tgs, int cellindex, int buttonindex)
{
if (!canAddNpc) return;
var clickPosition = _curMap.TGSCellIndex2BlockPosition(cellindex);
int leftButton = 0;
int rightButton = 1;
if (canAddNpc && buttonindex == leftButton)
{
if (npcObject == null)
{
DebugUtil.LogError("请选择正确的NPC");
return;
}
if (_npcDic.ContainsKey(clickPosition))
{
DebugUtil.LogError("该位置已有NPC存在请重新选");
return;
}
LevelData.NPCInfo npcInfo = new LevelData.NPCInfo();
string npcId = npcObject.name.Replace("P_E", "1");
npcInfo.id = int.Parse(npcId);
npcInfo.position = clickPosition;
//默认等级为1
npcInfo.level = 1;
CurrentNpcInfo.Add(npcInfo);
var position = _curMap.BlockPosition2WorldPosition(npcInfo.position);
_npcDic.Add(npcInfo.position, Instantiate(npcObject, position, npcObject.transform.rotation));
DebugUtil.Log("添加了[{0}]NPC到[ {1} ]位置上", npcObject.name, npcInfo.position);
}
else if(canAddNpc && buttonindex == rightButton)
{
if (_npcDic.TryGetValue(clickPosition,out var npcObj))
{
var npc = CurrentNpcInfo.FirstOrDefault(npc => npc.position == clickPosition);
if (npc != null)
{
CurrentNpcInfo.Remove(npc);
DebugUtil.Log("从[ {0} ]位置上移除了[{1}]NPC", clickPosition, npcObj.name);
DestroyImmediate(npcObj);
_npcDic.Remove(clickPosition);
}
}
}
}
private void DestroyNpcPrefab()
{
foreach (var npc in _npcDic)
{
DestroyImmediate(npc.Value);
}
}