KopMap/Assets/MindPowerSdk/EditorWindow/MapDataEditorWindow.cs

532 lines
23 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEngine;
namespace MindPowerSdk.Editor
{
/// <summary>
/// 地图生成工具窗口,用于配置地图数据参数、材质映射,以及生成、清除、保存地图到预制体。
/// 注意:每个地图配置项对应一个 MapName 与资源数据名称组合生成一个地图。
/// </summary>
public class MapGeneratorEditorWindow : UnityEditor.EditorWindow
{
// 文件夹路径设置
private string mapDataFolder = "";
private string resourceDataFolder = "";
// 地图数据配置:每个地图对应一个名称与数据资源名称
private List<MapInfoEntry> mapEntries = new List<MapInfoEntry>();
// 其他地图参数
private int chunkSize = 64;
private Material terrainMaterial;
// 材质映射设置(草、树等),关键字支持多个,用逗号分隔
private List<MaterialMappingEntry> materialMappings = new List<MaterialMappingEntry>();
// 生成地图时的父物体名称
private const string GENERATED_MAPS_PARENT = "GeneratedMaps";
[MenuItem("Tools/地图生成工具")]
public static void ShowWindow()
{
GetWindow<MapGeneratorEditorWindow>("地图生成工具");
}
private void OnEnable()
{
// 初始化默认数据
if (mapEntries == null) mapEntries = new List<MapInfoEntry>();
if (materialMappings == null) materialMappings = new List<MaterialMappingEntry>();
}
private void OnGUI()
{
GUILayout.Label("文件夹路径设置", EditorStyles.boldLabel);
// 数据源文件夹选择
EditorGUILayout.BeginHorizontal();
// 从持久化文件中读取
mapDataFolder = EditorPrefs.GetString("MapDataFolder", mapDataFolder);
resourceDataFolder = EditorPrefs.GetString("ResourceDataFolder", resourceDataFolder);
mapDataFolder = EditorGUILayout.TextField("数据源文件夹", mapDataFolder);
if (GUILayout.Button("选择", GUILayout.Width(60)))
{
string folder = EditorUtility.OpenFolderPanel("选择数据源文件夹", "", "");
if (!string.IsNullOrEmpty(folder))
{
mapDataFolder = folder;
}
}
EditorGUILayout.EndHorizontal();
// 资源数据文件夹选择
EditorGUILayout.BeginHorizontal();
resourceDataFolder = EditorGUILayout.TextField("资源数据文件夹", resourceDataFolder);
if (GUILayout.Button("选择", GUILayout.Width(60)))
{
string folder = EditorUtility.OpenFolderPanel("选择资源数据文件夹", "", "");
if (!string.IsNullOrEmpty(folder))
{
resourceDataFolder = folder;
}
}
EditorPrefs.SetString("ResourceDataFolder", resourceDataFolder);
EditorPrefs.SetString("MapDataFolder", mapDataFolder);
EditorGUILayout.EndHorizontal();
GUILayout.Space(10);
GUILayout.Label("地图数据配置 (每个配置项对应一个地图)", EditorStyles.boldLabel);
// 列表显示地图配置项
for (int i = 0; i < mapEntries.Count; i++)
{
EditorGUILayout.BeginHorizontal();
mapEntries[i].mapName = EditorGUILayout.TextField("Map 名称", mapEntries[i].mapName).Replace(".map", "");
mapEntries[i].resourceDataName = EditorGUILayout.TextField("资源名称", mapEntries[i].resourceDataName)
.Replace(".obj", "");
mapEntries[i].isSmoothing = EditorGUILayout.Toggle("对地形平滑处理", mapEntries[i].isSmoothing);
if (GUILayout.Button("删除", GUILayout.Width(60)))
{
mapEntries.RemoveAt(i);
i--;
}
EditorGUILayout.EndHorizontal();
}
if (GUILayout.Button("添加地图配置"))
{
mapEntries.Add(new MapInfoEntry());
}
GUILayout.Space(10);
GUILayout.Label("地图其他参数", EditorStyles.boldLabel);
chunkSize = EditorGUILayout.IntField("Chunk Size", chunkSize);
terrainMaterial =
(Material)EditorGUILayout.ObjectField("Terrain Material", terrainMaterial, typeof(Material), false);
GUILayout.Space(10);
GUILayout.Label("材质映射配置 (草、树等)", EditorStyles.boldLabel);
// 材质映射配置项
for (int i = 0; i < materialMappings.Count; i++)
{
EditorGUILayout.BeginHorizontal();
// 关键字输入,支持多个关键字,用逗号分隔
materialMappings[i].keywords = EditorGUILayout.TextField("匹配关键字(,分隔)", materialMappings[i].keywords);
materialMappings[i].targetMaterial =
(Material)EditorGUILayout.ObjectField("目标材质", materialMappings[i].targetMaterial, typeof(Material),
false);
if (GUILayout.Button("删除", GUILayout.Width(60)))
{
materialMappings.RemoveAt(i);
i--;
}
EditorGUILayout.EndHorizontal();
}
if (GUILayout.Button("添加材质映射配置"))
{
materialMappings.Add(new MaterialMappingEntry());
}
GUILayout.Space(10);
// 操作按钮
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("生成到场景"))
{
GenerateMaps();
}
if (GUILayout.Button("清除生成地图"))
{
ClearGeneratedMaps();
}
if (GUILayout.Button("保存为预制体"))
{
SaveMapsAsPrefabs();
}
EditorGUILayout.EndHorizontal();
}
/// <summary>
/// 根据当前配置生成所有地图,并生成到场景下的 GENERATED_MAPS_PARENT 父物体内。
/// </summary>
private void GenerateMaps()
{
// 检查必要的路径和配置
if (string.IsNullOrEmpty(mapDataFolder) || string.IsNullOrEmpty(resourceDataFolder))
{
Debug.LogError("请先设置数据源和资源数据的文件夹路径!");
return;
}
if (mapEntries.Count == 0)
{
Debug.LogError("请添加至少一个地图配置项!");
return;
}
// 查找或创建生成地图的父物体
GameObject parent = GameObject.Find(GENERATED_MAPS_PARENT);
if (parent == null)
{
parent = new GameObject(GENERATED_MAPS_PARENT);
}
// 遍历每个地图配置项生成地图
foreach (var entry in mapEntries)
{
// 拼接完整数据路径
string mapDataPath = Path.Combine(mapDataFolder, entry.mapName + ".map");
string resourceDataPath = Path.Combine(resourceDataFolder, entry.resourceDataName + ".obj");
// 读取地图数据
using FileStream fs = File.OpenRead(mapDataPath);
BinaryReader reader = new BinaryReader(fs);
var map = new MPMap();
map.Load(reader);
Debug.Log($"map size:({map.Width},{map.Height}) | section size:({map.SectionWidth},{map.SectionHeight})");
// 调用生成 Terrain 的方法,传入 mapName 作为标识
var textureList = TerrainGenerator.GenTerrain(map, chunkSize, terrainMaterial, parent.transform,
entry.mapName,
entry.isSmoothing);
Debug.Log($"生成地图:{entry.mapName}\n数据路径{mapDataPath}\n资源路径{resourceDataPath}");
// 创建场景对象并关联到地形
CreateMapObjectsWithTerrain(resourceDataPath, map, parent.transform, entry.mapName, chunkSize);
}
}
/// <summary>
/// 创建场景对象并智能地关联到地形系统
/// </summary>
/// <param name="objPath">场景对象文件路径</param>
/// <param name="map">地图数据</param>
/// <param name="parent">父物体</param>
/// <param name="mapName">地图名称</param>
/// <param name="chunkSize">区块大小</param>
void CreateMapObjectsWithTerrain(string objPath, MPMap map, Transform parent, string mapName, int chunkSize)
{
// 加载场景对象数据
CSceneObjSet sceneObjSet = new CSceneObjSet();
sceneObjSet.LoadBin("Assets/Resources/sceneobjinfo.bin");
SceneObjFile sceneObjFile = new SceneObjFile();
sceneObjFile.Load(objPath);
// 找到地形根节点
Transform terrainRoot = parent.Find(mapName);
if (terrainRoot == null)
{
Debug.LogError($"找不到地形根节点: {mapName}");
return;
}
// 创建场景对象根节点
string objName = Path.GetFileNameWithoutExtension(objPath);
GameObject sceneRoot = new GameObject($"{objName}_SceneObjects");
sceneRoot.transform.SetParent(parent);
// 收集所有地形组件,用于场景对象关联
Dictionary<Vector2Int, Terrain> terrainMap = new Dictionary<Vector2Int, Terrain>();
CollectTerrainComponents(terrainRoot, terrainMap, chunkSize);
// 遍历所有场景对象
for (int y = 0; y < sceneObjFile.FileHeader.SectionCntY; y++)
{
for (int x = 0; x < sceneObjFile.FileHeader.SectionCntX; x++)
{
var objList = sceneObjFile.objInfos[x, y];
if (objList == null) continue;
foreach (var sceneObjInfo in objList)
{
if (sceneObjInfo.GetTypeId() == 1) continue; // 跳过特效物件
int id = sceneObjInfo.GetID();
CSceneObjInfo modeInfo = sceneObjSet.Get(id);
// 计算世界坐标,确保与地形坐标系一致
// 地图坐标系转换X不变Z轴翻转以匹配地形位置计算
float objX = sceneObjInfo.X / 100f;
float objY = sceneObjInfo.HeightOff / 100f;
float objZ = map.Height - (sceneObjInfo.Y / 100f); // 与地形位置计算保持一致
Vector3 worldPos = new Vector3(objX, objY, objZ);
// 获取地形高度
float mapHeight = map.GetHeight(worldPos.x, sceneObjInfo.Y / 100f);
worldPos.y += mapHeight;
// 加载模型资源
string modelPath = "Assets/Resources/Model/Scene/" +
Path.GetFileNameWithoutExtension(modeInfo.szDataName) + ".lmo.obj";
GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>(modelPath);
if (prefab == null)
{
Debug.LogError($"模型未找到: {modelPath}");
continue;
}
// 简化处理:统一作为场景对象处理,确保正确的地形贴合
GameObject instance = UnityEngine.Object.Instantiate(prefab);
instance.transform.position = worldPos;
instance.transform.localScale = new Vector3(1, 1, -1);
Vector3 euler = instance.transform.eulerAngles;
instance.transform.rotation = Quaternion.Euler(euler.x, sceneObjInfo.YawAngle, euler.z);
instance.name = $"[{id}]{prefab.name}";
instance.transform.SetParent(sceneRoot.transform);
// 添加地形关联信息(可选)
// 计算该对象属于哪个地形块
int terrainX = Mathf.FloorToInt(worldPos.x / chunkSize);
// 因为我们的地形Z位置是 map.Height - startY - resY所以需要反向计算
int originalMapY = (int)(map.Height - worldPos.z);
int terrainY = Mathf.FloorToInt(originalMapY / chunkSize);
Vector2Int terrainCoord = new Vector2Int(terrainX, terrainY);
if (terrainMap.TryGetValue(terrainCoord, out Terrain associatedTerrain))
{
// 可以在这里添加对象与地形的关联逻辑
// 例如:给对象添加一个组件来记录关联的地形
Debug.Log($"对象 {instance.name} 关联到地形: {associatedTerrain.name}");
}
}
}
}
Debug.Log($"场景对象处理完成 - 总对象数: {sceneRoot.transform.childCount}");
}
/// <summary>
/// 收集所有地形组件
/// </summary>
void CollectTerrainComponents(Transform terrainRoot, Dictionary<Vector2Int, Terrain> terrainMap, int chunkSize)
{
foreach (Transform child in terrainRoot)
{
Terrain terrain = child.GetComponent<Terrain>();
if (terrain != null)
{
// 从地形名称解析坐标 (terrain_i_j)
string[] parts = child.name.Split('_');
if (parts.Length >= 3 &&
int.TryParse(parts[1], out int i) &&
int.TryParse(parts[2], out int j))
{
// 存储地形块的索引坐标和对应的Terrain组件
Vector2Int coord = new Vector2Int(j, i); // j是x索引, i是y索引
terrainMap[coord] = terrain;
Debug.Log($"收集地形块: {child.name} -> 坐标({j}, {i}) -> 世界位置{terrain.transform.position}");
}
}
}
}
/// <summary>
/// 清除场景中已生成的地图(父物体名称为 GENERATED_MAPS_PARENT
/// </summary>
private void ClearGeneratedMaps()
{
GameObject parent = GameObject.Find(GENERATED_MAPS_PARENT);
if (parent != null)
{
DestroyImmediate(parent);
Debug.Log("已清除生成的地图");
}
else
{
Debug.LogWarning("未找到生成的地图");
}
}
/// <summary>
/// 将生成的每个地图(即每个 MapName 对应的 Terrain 节点)保存为独立预制体,存储在 Assets/Prefabs 下。
/// </summary>
private void SaveMapsAsPrefabs()
{
#if UNITY_EDITOR
GameObject parent = GameObject.Find(GENERATED_MAPS_PARENT);
if (parent == null)
{
Debug.LogWarning("没有生成的地图可以保存");
return;
}
// 遍历每个地图生成的子对象
foreach (Transform mapTransform in parent.transform)
{
if (mapTransform.name.Contains("SceneObjects") || mapTransform.name.Contains("SceneAssetsRoot"))
{
continue;
}
string folder = EditorUtility.OpenFolderPanel("选择预制体路径", "", "") + $"/{mapTransform.name}/";
string prefabPath = folder + mapTransform.name + ".prefab";
string directory = Path.GetDirectoryName(prefabPath);
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
string materialDir = Path.GetDirectoryName(prefabPath) + "/Material";
if (!Directory.Exists(materialDir))
{
Directory.CreateDirectory(materialDir);
}
string texturesDir = Path.GetDirectoryName(prefabPath) + "/Textures";
if (!Directory.Exists(texturesDir))
{
Directory.CreateDirectory(texturesDir);
}
for (int i = 0; i < mapTransform.childCount; i++)
{
var child = mapTransform.GetChild(i);
var terrain = child.GetComponent<Terrain>();
var terrainCollider = child.GetComponent<TerrainCollider>();
Debug.Log(child.name);
if (terrain != null)
{
materialDir = materialDir.Replace(@"\", "/");
materialDir = materialDir.Replace(Application.dataPath, "Assets");
var materialFiled = $"{materialDir}/{mapTransform.name}_{child.name}.mat";
var terrainDataPath = directory.Replace(@"\", "/");
terrainDataPath = terrainDataPath.Replace(Application.dataPath, "Assets");
terrainDataPath = $"{terrainDataPath}/{mapTransform.name}_{child.name}.asset";
if (File.Exists(materialFiled))
AssetDatabase.DeleteAsset(materialFiled);
if (File.Exists(terrainDataPath))
AssetDatabase.DeleteAsset(terrainDataPath);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
// 保存材质和地形数据
AssetDatabase.CreateAsset(terrain.materialTemplate, materialFiled);
AssetDatabase.CreateAsset(terrainCollider.terrainData, terrainDataPath);
// 确保地形数据的引用正确
terrain.terrainData = terrainCollider.terrainData;
// 把 terrain.materialTemplate 中所有的图片保存下来
foreach (var texturePropertyName in terrain.materialTemplate.GetTexturePropertyNames())
{
var texture = terrain.materialTemplate.GetTexture(texturePropertyName);
if (texture)
{
Debug.Log($"保存材质贴图:{texturePropertyName} | {texture.name}");
string texturePath = texturesDir.Replace(@"\", "/");
texturePath = texturePath.Replace(Application.dataPath, "Assets");
var textName = $"{texturePropertyName}_{child.name}";
texturePath = $"{texturePath}/{textName}.png";
// 检查纹理是否已经是项目中的资产
string originalPath = AssetDatabase.GetAssetPath(texture);
if (!string.IsNullOrEmpty(originalPath))
{
// 如果是已存在的资产,复制到目标位置
if (!File.Exists(texturePath))
{
AssetDatabase.CopyAsset(originalPath, texturePath);
Debug.Log($"复制材质贴图:{originalPath} -> {texturePath}");
}
}
else
{
// 如果是运行时生成的纹理保存为PNG文件
if (texture is Texture2D texture2D)
{
byte[] bytes = texture2D.EncodeToPNG();
if (bytes != null)
{
File.WriteAllBytes(texturePath.Replace("Assets", Application.dataPath),
bytes);
AssetDatabase.ImportAsset(texturePath);
Debug.Log($"导出材质贴图:{texturePath}");
}
}
}
}
}
}
}
PrefabUtility.SaveAsPrefabAssetAndConnect(mapTransform.gameObject, prefabPath,
InteractionMode.UserAction);
Debug.Log($"保存预制体:{prefabPath}");
}
#else
Debug.LogWarning("该功能仅在 Unity Editor 下可用");
#endif
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
}
/// <summary>
/// 根据传入的类型名称(如"草"或"树")进行模糊匹配,
/// 遍历材质映射配置,返回第一个匹配的目标材质,未匹配返回 null。
/// </summary>
public Material GetMappedMaterial(string typeName)
{
foreach (var mapping in materialMappings)
{
if (!string.IsNullOrEmpty(mapping.keywords))
{
// 以逗号分隔关键字,并检查 typeName 是否包含任一关键字
string[] keys = mapping.keywords.Split(',');
foreach (var key in keys)
{
if (typeName.Contains(key.Trim()))
{
return mapping.targetMaterial;
}
}
}
}
return null;
}
}
/// <summary>
/// 地图配置项,每个项对应一个 Map 名称与资源数据名称组合
/// </summary>
[System.Serializable]
public class MapInfoEntry
{
public string mapName = "";
public string resourceDataName = "";
public bool isSmoothing = true; // 是否平滑处理
}
/// <summary>
/// 材质映射配置项,用于配置多个关键字与目标材质的对应关系
/// </summary>
[System.Serializable]
public class MaterialMappingEntry
{
// 支持多个匹配关键字,用逗号分隔
public string keywords = "";
public Material targetMaterial;
}
}