/* * @Author: 白哉 * @Description: 地图生成器 * @Date: 2025年04月01日 星期二 11:04:43 * @Modify: */ using UnityEngine; using System; using System.Collections.Generic; using System.Linq; using Logic.Pool; using RuntimeData; using TH1_Logic.Core; using Unity.VisualScripting; using Random = UnityEngine.Random; namespace Logic { public enum MapWaterType { Dryland, // 0.05 Lakes, // 0.15 Pangea, // 0.3 Continents, // 0.5 Archipelago, // 0.7 WaterWorld // 0.9 } public static class MapWaterTypeHelper { public static float GetLandThreshold(MapWaterType waterType) { return waterType switch { MapWaterType.Dryland => 0.05f, MapWaterType.Lakes => 0.15f, MapWaterType.Pangea => 0.3f, MapWaterType.Continents => 0.5f, MapWaterType.Archipelago => 0.7f, MapWaterType.WaterWorld => 0.9f, _ => 0.3f }; } } public class MapGenerator { private static List _aroundBuf; private Main _main; private MapData _mapData; //海陆层参数,柏林噪音 private const float Scale = 20f; // 噪声的缩放因子 private float _landThreshold; // 陆地的阈值 private float _mountainThreshold = 0.7f; // 山地的阈值 private float[,] _heightMap; private const int SmoothIterations = 3; //平滑次数 private const float NearbyEnemyCapitalRateForRealPlayer = 0.15f; private const int NearbyEnemyCapitalDistance = 3; private const int SeaStartStarfishRadius = 2; private const int SeaStartStarfishMinCount = 4; private const int SeaStartFishRadius = 1; private const int SeaStartExtraFishCount = 2; private const int SeaStartOuterFishRadius = 2; private const int SeaStartOuterFishExtraCount = 3; private const int SeaStartIslandCheckRadius = 2; private const int SeaStartIslandMaxLandCount = 4; private uint _width, _height; private List _tribes; //用来避免treasure连着生成 private List _treasure; private List _innerCityGrid; private List _outerCityGrid; public List PlayerCivOri; List dir4 = new List() { Vector2.up, Vector2.right, Vector2.down, Vector2.left }; List dir8 = new List() { Vector2.down , Vector2.right , Vector2.left , Vector2.up ,Vector2.left+Vector2.up , Vector2.up+Vector2.right, Vector2.down+Vector2.right,Vector2.down+Vector2.left }; // 初始化地图 public MapGenerator(Main main,MapData mapData) { _main = main; _mapData = mapData; Table.Instance.GeoDataAssets.OnMapGenerator(); } public void GenerateMap(MapData mapData) { _width = mapData.MapConfig.Width; _height = mapData.MapConfig.Height; _heightMap = new float[_width, _height]; if (DebugCenter.Instance.DebugLandThreshold >= 0) _landThreshold = DebugCenter.Instance.DebugLandThreshold; else _landThreshold = MapWaterTypeHelper.GetLandThreshold(mapData.MapConfig.WaterType); _tribes = new List(); _treasure = new List(); _innerCityGrid = new List(); _outerCityGrid = new List(); //用柏林噪音生成地图高度 GenerateHeightMap(); //平滑地图 SmoothHeightMap(); //归一化处理地图,均匀分布0~1范围 NormalizeHeightMap(); //Dryland模式:强制将所有水域格子抬升为陆地,地图上不存在任何水域 if (mapData.MapConfig.WaterType == MapWaterType.Dryland) { for (var x = 0; x < _width; x++) for (var y = 0; y < _height; y++) if (_heightMap[x, y] <= _landThreshold) _heightMap[x, y] = _landThreshold + 0.01f; } //在连续的大片海洋上生成岛屿 GenerateIsland(); //在连续的陆地上生成湖泊 GenerateLakes(); //生成所有tribe GenerateTribes(); //创建文明摇篮城市(每个玩家的原始首都) GenerateCradleCity(mapData); //初始化所有Grid信息 InitGridMapData(mapData); //修复摇篮城市附近的地形保底和资源保底 CivTerrainFeatureCountControl(mapData); //生成地脉(所有资源确定后执行) GenerateLeyLines(mapData); //TODO 暂时屏蔽地脉生成 //生成所有GeoInf Table.Instance.GeoDataAssets.InitGeoData(mapData); } //在MapRenderer初始化之后,再进行一些mapgenerator的其他操作 public void GenerateMapAfterMapRenderer(MapData mapData) { //初始化所有玩家的初始视野信息 InitAllPlayerSight(mapData); //初始化Debug的参数 InitDebugInfo(mapData); //初始化所有玩家的特殊领地技能(目前主要是kaguya) InitPlayerSpecialSkill(mapData); } //海陆层使用的地形高度数据 private void GenerateHeightMap() { for (var x = 0; x < _width; x++) { for (var y = 0; y < _height; y++) { float seda = new System.Random().Next(1, 30); float sedb = new System.Random().Next(2, 40); // 使用种子值来改变噪声的生成,保证每次生成的噪声不同 var xCoord = (float)x / _width * Scale / seda; var yCoord = (float)y / _height * Scale / sedb; _heightMap[x, y] = Mathf.PerlinNoise(xCoord, yCoord); //Debug.Log($"sed = {seda}\\{sedb} xC = {xCoord} yC = {yCoord} height = {heightMap[x, y]}"); } } } //海陆层使用的地形平滑工具 private void SmoothHeightMap() { for (var iteration = 0; iteration < SmoothIterations; iteration++) { var newHeightMap = new float[_width, _height]; for (int x = 0; x < _width; x++) { for (int y = 0; y < _height; y++) { // 计算该位置的邻域平均值 var average = GetAverageHeight(x, y); newHeightMap[x, y] = average; } } _heightMap = newHeightMap; } } // 获取该位置的邻域平均高度值 private float GetAverageHeight(int x, int y) { var total = 0f; var count = 0; // 获取周围的邻域高度值 for (var dx = -1; dx <= 1; dx++) { for (var dy = -1; dy <= 1; dy++) { var nx = x + dx; var ny = y + dy; if (nx < 0 || nx >= _width || ny < 0 || ny >= _height) continue; total += _heightMap[nx, ny]; count++; // 确保在有效范围内 } } return total / count; } //归一化处理高度 private void NormalizeHeightMap() { var minHeight = Mathf.Infinity; var maxHeight = -Mathf.Infinity; // 计算最小值和最大值 for (var x = 0; x < _width; x++) { for (int y = 0; y < _height; y++) { if (_heightMap[x, y] < minHeight) minHeight = _heightMap[x, y]; if (_heightMap[x, y] > maxHeight) maxHeight = _heightMap[x, y]; } } // 将高度图归一化到 [0, 1] 范围 for (var x = 0; x < _width; x++) { for (var y = 0; y < _height; y++) { _heightMap[x, y] = (_heightMap[x, y] - minHeight) / (maxHeight - minHeight); } } } //生成湖泊 private void GenerateLakes() { //Step #0 根据WaterType决定河流上限 var waterType = _mapData.MapConfig.WaterType; if (waterType == MapWaterType.Dryland) return; // 干旱:不生成河流 //Step #1 每次寻找一个最高的山,开始制造河流,每次流往附近最低的格子,直到格子周围没有比自己更低的格子,停下。 //每次找的山头,周围2格内不能有水流 List<(Vector2Int pos, float height)> sorth = GetSortedTilesByHeight(_heightMap); //目前先暂定river最多占掉0.05的陆地格子 int riverCount = (int)(sorth.Count * 0.05); for (int i = sorth.Count - 1; i >= 0; i--) if(sorth[i].height <= _landThreshold) { riverCount = (int)((sorth.Count - i) * 0.05); if (i * 2 < _width * _height) { //30%的概率是干旱图,70%的概率是多河图 if (Random.Range(0f, 1f) < 0.7) { riverCount = (int)((sorth.Count - i) * 0.2); //Debug.Log("More River!!"); } } break; } // 湖泊模式:最多生成1条河流的长度 if (waterType == MapWaterType.Lakes) riverCount = Mathf.Min(riverCount, 1); for(int i = sorth.Count - 1;i >= 0 && riverCount > 0; i-- ) { //寻找下一个河流源头 if (!CheckGridRiverHead(sorth[i].pos.x, sorth[i].pos.y, 2)) continue; var x = sorth[i].pos.x; var y = sorth[i].pos.y; for (;riverCount > 0;)//p用来保底 { //当前水流x,y,找四个方向最低的 int tx = x, ty = y; if (x - 1 >= 0 && _heightMap[x - 1, y] < _heightMap[tx, ty] && _heightMap[x - 1, y] > _landThreshold) {tx = x - 1; ty = y; } if (x + 1 < _width && _heightMap[x + 1, y] < _heightMap[tx, ty] && _heightMap[x + 1, y] > _landThreshold) {tx = x + 1; ty = y; } if (y - 1 >= 0 && _heightMap[x, y - 1] < _heightMap[tx, ty]&& _heightMap[x, y - 1] > _landThreshold) {tx = x; ty = y - 1; } if (y + 1 < _height && _heightMap[x, y + 1] < _heightMap[tx, ty] && _heightMap[x, y + 1] > _landThreshold) {tx = x; ty = y + 1; } //让当前格子变为水流 _heightMap[x, y] = 0; riverCount--; //如果不能再流 if (tx == x && ty == y) break; x = tx; y = ty; } } } //给生成湖泊用的函数 给格子排序高度 public static List<(Vector2Int pos, float height)> GetSortedTilesByHeight(float[,] heights) { int width = heights.GetLength(0); int height = heights.GetLength(1); // 使用 LINQ 将二维数组展平、转换、排序并生成列表,一行搞定。 return Enumerable.Range(0, width) .SelectMany(x => Enumerable.Range(0, height) .Select(y => (pos: new Vector2Int(x, y), height: heights[x, y]))) .OrderBy(tile => tile.height) .ToList(); } //给生成湖泊用的函数 ,确认一个点周围x范围内有没有水 public bool CheckGridRiverHead(int X,int Y,int r) { for (var x = X - r; x < X + r; x++) for (var y = Y - r; y < Y + r; y++) { if(x < 0 || x >= _width || y < 0 || y >= _height) return false; if (_heightMap[x, y] <= _landThreshold) return false; } return true; } //在连续大片海洋中生成岛屿 private void GenerateIsland() { //WaterWorld模式下大幅降低岛屿生成概率(10%),其他模式50% int islandChance = _mapData.MapConfig.WaterType == MapWaterType.WaterWorld ? 10 : 50; for (var x = 1; x < _width - 1; x++) for (var y = 1; y < _height - 1; y++) { var t = GenerateTerrain(x, y); var canBeIsland = false; if (t == TerrainType.DeepSea) { canBeIsland = true; var landNearby = 0; for (var i = -2; i <= 2; i++) for (var j = -2; j <= 2; j++) if (x + i >= 0 && x + i < _width && y + j >= 0 && y + j < _height && GenerateTerrain(x + i, y + j) != TerrainType.DeepSea) landNearby++; if (landNearby > 9) canBeIsland = false; } if (canBeIsland && UnityEngine.Random.Range(0, 100) < islandChance) _heightMap[x, y] = _landThreshold + 1; } } //生成所有tirbes 以及对应的innergrids private void GenerateTribes() { var cant = new bool[_width, _height]; var count = _width * _height; //剩余多少格子能用 //WaterWorld模式下按陆地格子数计算村庄数量,避免小面积陆地上村庄过密 //同时保证最少有玩家数*2个村庄位,确保所有阵营能正常生成 int cityNum; if (_mapData.MapConfig.WaterType == MapWaterType.WaterWorld) { int landTiles = 0; for (var x = 0; x < _width; x++) for (var y = 0; y < _height; y++) if (_heightMap[x, y] > _landThreshold) landTiles++; int minCities = (int)_mapData.MapConfig.PlayerCount * 2; cityNum = Mathf.Max(landTiles / 18, minCities); } else { cityNum = (int)(count / 12); } Array.Clear(cant, 0, cant.Length); for (var x = 0; x < _width; x++) for (var y = 0; y < _height; y++) if (_heightMap[x, y] <= _landThreshold || x == 0 || x == _width - 1 || y == 0 || y == _height - 1) { cant[x, y] = true; count--; } for (int c = 0; count > 0 && c <= cityNum; c++) { var choose = (int)UnityEngine.Random.Range(1, count); //每次随机一个可用的格子(序号) //Debug.Log("this time " + count + " choose : " + choose); var tmp = 0; for (int x = 0; x < _width; x++) { for (int y = 0; y < _height; y++) { if (cant[x, y]) continue; tmp++; if (tmp != choose) continue; _tribes.Add(new MapPosition(x,y)); for (var xx = -2; xx <= 2; xx++) for (var yy = -2; yy <= 2; yy++) { var nx = x + xx; var ny = y + yy; if (nx < 0 || nx >= _width || ny < 0 || ny >= _height || cant[nx, ny]) continue; cant[nx, ny] = true; // 把新城市方圆2距离的格子都ban了 count--; } break; } if (tmp == choose) break; } if (count <= 0) break; } foreach (var cent in _tribes) { for(int i = cent.X - 1; i<= cent.X + 1 ; i++) for(int j = cent.Y - 1; j<= cent.Y + 1 ; j++) if(i >= 0 && i < _width && j >= 0 && j < _height) _innerCityGrid.Add(new MapPosition(i,j)); } foreach (var cent in _tribes) { for(int i = cent.X - 2; i<= cent.X + 2 ; i++) for(int j = cent.Y - 2; j<= cent.Y + 2 ; j++) if(i >= 0 && i < _width && j >= 0 && j < _height) _outerCityGrid.Add(new MapPosition(i,j)); } //List LandCenters = new List(); //_map.cityCount = map.cityCenters.Count; } private void InitGridMapData(MapData mapData) { //标记innerCity和OnterCity for (int x = 0; x < _width; x++) { for (int y = 0; y < _height; y++) { Vector2 curV2 = new Vector2(x, y); // 生成文明层 var civId = GenerateCivilization(mapData, x, y); CivEnum civEnum = (CivEnum)(civId + 1); InnerOuterCity innerOuterCity = InnerOuterCity.Edge; if (_innerCityGrid.Contains(new MapPosition(x, y))) innerOuterCity = InnerOuterCity.InnerCity; else if (_outerCityGrid.Contains(new MapPosition(x, y))) innerOuterCity = InnerOuterCity.OuterCity; // 生成海陆层(陆地、浅海、深海) var terrain = GenerateTerrain(x, y, civEnum); // 生成地形层(是否有山) var feature = (terrain == TerrainType.Land) ? GenerateFeature(x, y, civEnum) : TerrainFeature.None; //如果是摇篮城市,terrain设置为road if (mapData.GridMap.GetGridDataByPos(x, y, out var gg) && mapData.GetCityDataByGid(gg.Id, out var cc)) feature = TerrainFeature.Road; // 生成植被层(是否有树木) var vegetation = (feature == TerrainFeature.None && terrain == TerrainType.Land) ? GenerateVegetation(x, y, civEnum) : Vegetation.None; // 生成资源层 var resource = GenerateResource(x, y, terrain, feature, vegetation, civEnum, innerOuterCity); //生成四个角的塔 if (HasTower(x, y)) { feature = TerrainFeature.None; resource = ResourceType.Tower; vegetation = Vegetation.None; } // 设置这个地块的所有层信息 //Debug.Log(map.grid[x,y]); if (mapData.GridMap.GetGridDataByPos(x, y, out GridData g)) g.SetGridData(terrain, feature, vegetation, resource, civId); } } } private void CivTerrainFeatureCountControl(MapData mapData) { foreach (var city in mapData.CityMap.CityList) { var grid = city.Grid(mapData); var player = city.Player(mapData); if (grid == null) continue; if(player == null) continue; dir8 = new List() { Vector2.up, Vector2.down, Vector2.left, Vector2.right, Vector2.up + Vector2.right, Vector2.up + Vector2.left, Vector2.down + Vector2.left, Vector2.down + Vector2.right }; //随机打乱dir8的顺序,使得每次强制生成保底的时候,没有明显规律 int n = dir8.Count; while (n > 0) { n--; int k = Random.Range(0,n + 1); (dir8[k], dir8[n]) = (dir8[n], dir8[k]); } dir4 = new List() { Vector2.up, Vector2.down, Vector2.left, Vector2.right, Vector2.up + Vector2.right }; //处理land保底 if (Table.Instance.PlayerDataAssets.GetLandformCount(TerrainType.Land,player.CivEnum, out var landCount)) { var cur = grid.Pos.V2(); int count = 0; foreach (var dir in dir8) { if (!mapData.GridMap.GetGridDataByV2(cur + dir, out var tgrid)) continue; if (tgrid.Terrain == TerrainType.Land) count++; } //如果land太少 ,要补充 if (count < landCount) { foreach (var dir in dir8) { if (count >= landCount) break; if (!mapData.GridMap.GetGridDataByV2(cur + dir, out var tgrid)) continue; if (tgrid.Terrain != TerrainType.Land) { //将该海洋格子改为陆地格子 if (tgrid.Resource == ResourceType.Tower) continue; // 保护结界塔 tgrid.Terrain = TerrainType.Land; tgrid.Resource = ResourceType.None; //将该格子上下左右dir4方向的深海都变成浅海 foreach (var dirp in dir4) { if (!mapData.GridMap.GetGridDataByV2(cur + dir + dirp, out var ttgrid)) continue; if (ttgrid.Terrain == TerrainType.DeepSea) ttgrid.Terrain = TerrainType.ShallowSea; } count++; } } } } //处理mountain保底 if (Table.Instance.PlayerDataAssets.GetLandformCount(TerrainFeature.Mountain,player.CivEnum, out var mountainCount)) { var cur = grid.Pos.V2(); int count = 0; foreach (var dir in dir8) { if (!mapData.GridMap.GetGridDataByV2(cur + dir, out var tgrid)) continue; if (tgrid.Feature == TerrainFeature.Mountain) count++; } //Debug.Log($"原来只有{count}座山脉"); //如果mountain太少 ,要补充 if (count < mountainCount) { foreach (var dir in dir8) { if (count >= mountainCount) break; if (!mapData.GridMap.GetGridDataByV2(cur + dir, out var tgrid)) continue; if (tgrid.Resource == ResourceType.Tower) continue; if (tgrid.Terrain == TerrainType.Land && tgrid.Feature != TerrainFeature.Mountain) { //将该陆地格子改山脉格子 tgrid.Feature = TerrainFeature.Mountain; tgrid.Vegetation = Vegetation.None; //Debug.Log($"新增山脉位置:{cur+dir}"); count++; } } } //Debug.Log($"新增到{count}座山脉"); } //处理Tree保底 if (Table.Instance.PlayerDataAssets.GetLandformCount(Vegetation.Trees,player.CivEnum, out var treeCount)) { var cur = grid.Pos.V2(); int count = 0; foreach (var dir in dir8) { if (!mapData.GridMap.GetGridDataByV2(cur + dir, out var tgrid)) continue; if (tgrid.Vegetation == Vegetation.Trees) count++; } //Debug.Log($"原来只有{count}个森林"); //如果tree太少 ,要补充 if (count < treeCount) { foreach (var dir in dir8) { if (count >= treeCount) break; if (!mapData.GridMap.GetGridDataByV2(cur + dir, out var tgrid)) continue; if (tgrid.Resource == ResourceType.Tower) continue; if (tgrid.Terrain == TerrainType.Land && tgrid.Feature != TerrainFeature.Mountain && tgrid.Vegetation != Vegetation.Trees && tgrid.Resource == ResourceType.None) { //将该陆地格子改山森林格子 tgrid.Vegetation = Vegetation.Trees; //Debug.Log($"新增1个森林位于{cur+dir}"); count++; } } } //Debug.Log($"新增后有{count}个森林"); } //处理Resource保底 var resourceList = new List() {ResourceType.Fish, ResourceType.Animal, ResourceType.Fruit, ResourceType.Metal, ResourceType.Crop }; foreach (var resource in resourceList) { if (Table.Instance.PlayerDataAssets.GetLandformCount(resource,player.CivEnum, innerOuterCity:InnerOuterCity.InnerCity,out var recourceCount)) { var cur = grid.Pos.V2(); int count = 0; foreach (var dir in dir8) { if (!mapData.GridMap.GetGridDataByV2(cur + dir, out var tgrid)) continue; if (tgrid.Resource == resource) count++; } //Debug.Log($"原来只有{count}个{resource}"); //如果resource太少,要补充 if (count < recourceCount) { //第一轮:在满足条件的格子上放置资源 foreach (var dir in dir8) { if (count >= recourceCount) break; if (!mapData.GridMap.GetGridDataByV2(cur + dir, out var tgrid)) continue; if (tgrid.Resource == ResourceType.Tower) continue; switch (resource) { case ResourceType.Fish: if (tgrid.Terrain != TerrainType.Land && tgrid.Resource != ResourceType.Fish) { tgrid.Resource = ResourceType.Fish; count++; } break; case ResourceType.Fruit: if (tgrid.Terrain == TerrainType.Land && tgrid.Vegetation != Vegetation.Trees && tgrid.Feature != TerrainFeature.Mountain && tgrid.Resource != ResourceType.Fruit) { tgrid.Resource = ResourceType.Fruit; count++; } break; case ResourceType.Animal: if (tgrid.Vegetation == Vegetation.Trees && tgrid.Resource != ResourceType.Animal) { tgrid.Resource = ResourceType.Animal; count++; } break; case ResourceType.Metal: if (tgrid.Feature == TerrainFeature.Mountain && tgrid.Resource != ResourceType.Metal) { tgrid.Resource = ResourceType.Metal; count++; } break; case ResourceType.Crop: if (tgrid.Terrain == TerrainType.Land && tgrid.Vegetation != Vegetation.Trees && tgrid.Feature != TerrainFeature.Mountain && tgrid.Resource != ResourceType.Crop) { tgrid.Resource = ResourceType.Crop; count++; } break; default: break; } } //第二轮:如果第一轮没有凑够,强制改造地形来放置资源 if (count < recourceCount) { foreach (var dir in dir8) { if (count >= recourceCount) break; if (!mapData.GridMap.GetGridDataByV2(cur + dir, out var tgrid)) continue; if (tgrid.Resource == ResourceType.Tower) continue; if (tgrid.Resource == ResourceType.CityCenter) continue; switch (resource) { case ResourceType.Animal: //Animal需要树林,如果当前是陆地平地无树,强制种树并放动物 if (tgrid.Terrain == TerrainType.Land && tgrid.Feature != TerrainFeature.Mountain && tgrid.Vegetation != Vegetation.Trees && tgrid.Resource != ResourceType.Animal) { tgrid.Vegetation = Vegetation.Trees; tgrid.Resource = ResourceType.Animal; count++; } break; case ResourceType.Metal: //Metal需要山脉,如果当前是陆地无山,强制造山并放矿 if (tgrid.Terrain == TerrainType.Land && tgrid.Feature != TerrainFeature.Mountain && tgrid.Resource != ResourceType.Metal) { tgrid.Feature = TerrainFeature.Mountain; tgrid.Vegetation = Vegetation.None; tgrid.Resource = ResourceType.Metal; count++; } break; default: break; } } } } //Debug.Log($"新增后有{count}个{resource}"); } } if (IsSeaStartForResourceBonus(mapData, player, grid)) { EnsureSeaStartResourceCount( mapData, grid, ResourceType.Starfish, SeaStartStarfishRadius, SeaStartStarfishMinCount, SeaStartStarfishRadius); AddSeaStartResource(mapData, grid, ResourceType.Fish, SeaStartFishRadius, SeaStartExtraFishCount); PlaceSeaStartResource( mapData, grid, ResourceType.Fish, SeaStartOuterFishRadius, SeaStartOuterFishExtraCount, SeaStartOuterFishRadius, false); } } } private bool IsSeaStartForResourceBonus(MapData mapData, PlayerData player, GridData centerGrid) { if (player == null || centerGrid == null) return false; if (!IsOriginalLand(centerGrid.Pos.X, centerGrid.Pos.Y)) return true; if (!Table.Instance.PlayerDataAssets.GetLandformCount(TerrainType.Land, player.CivEnum, out var landCount)) landCount = 0; if (landCount > 0 && CountOriginalLandAround(centerGrid, 1) < landCount) return true; if (CountOriginalLandAround(centerGrid, SeaStartIslandCheckRadius, true) <= SeaStartIslandMaxLandCount) return true; return CountCurrentLandAround(mapData, centerGrid, SeaStartIslandCheckRadius, true) <= SeaStartIslandMaxLandCount; } private int CountOriginalLandAround(GridData centerGrid, int radius, bool includeCenter = false) { var count = 0; for (int x = centerGrid.Pos.X - radius; x <= centerGrid.Pos.X + radius; x++) for (int y = centerGrid.Pos.Y - radius; y <= centerGrid.Pos.Y + radius; y++) { if (!includeCenter && x == centerGrid.Pos.X && y == centerGrid.Pos.Y) continue; if (!IsOriginalLand(x, y)) continue; count++; } return count; } private int CountCurrentLandAround(MapData mapData, GridData centerGrid, int radius, bool includeCenter = false) { var count = 0; for (int x = centerGrid.Pos.X - radius; x <= centerGrid.Pos.X + radius; x++) for (int y = centerGrid.Pos.Y - radius; y <= centerGrid.Pos.Y + radius; y++) { if (!includeCenter && x == centerGrid.Pos.X && y == centerGrid.Pos.Y) continue; if (!mapData.GridMap.GetGridDataByPos(x, y, out var grid)) continue; if (grid.Terrain != TerrainType.Land) continue; count++; } return count; } private bool IsOriginalLand(int x, int y) { if (x < 0 || y < 0 || x >= (int)_width || y >= (int)_height) return false; return _heightMap[x, y] > _landThreshold; } private void EnsureSeaStartResourceCount( MapData mapData, GridData centerGrid, ResourceType resource, int radius, int minCount, int preferredMinDistance) { var needCount = minCount - CountResourceAround(mapData, centerGrid, resource, radius); if (needCount <= 0) return; PlaceSeaStartResource(mapData, centerGrid, resource, radius, needCount, preferredMinDistance, resource == ResourceType.Starfish); } private void AddSeaStartResource(MapData mapData, GridData centerGrid, ResourceType resource, int radius, int addCount) { if (addCount <= 0) return; PlaceSeaStartResource(mapData, centerGrid, resource, radius, addCount, 0, false); } private int CountResourceAround(MapData mapData, GridData centerGrid, ResourceType resource, int radius) { var count = 0; for (int x = centerGrid.Pos.X - radius; x <= centerGrid.Pos.X + radius; x++) for (int y = centerGrid.Pos.Y - radius; y <= centerGrid.Pos.Y + radius; y++) { if (x == centerGrid.Pos.X && y == centerGrid.Pos.Y) continue; if (Mathf.Max(Mathf.Abs(x - centerGrid.Pos.X), Mathf.Abs(y - centerGrid.Pos.Y)) > radius) continue; if (!mapData.GridMap.GetGridDataByPos(x, y, out var grid)) continue; if (grid.Resource == resource) count++; } return count; } private int PlaceSeaStartResource( MapData mapData, GridData centerGrid, ResourceType resource, int radius, int addCount, int preferredMinDistance, bool allowReplaceFish) { var preferredNone = new List(); var fallbackNone = new List(); var preferredFish = new List(); var fallbackFish = new List(); for (int x = centerGrid.Pos.X - radius; x <= centerGrid.Pos.X + radius; x++) for (int y = centerGrid.Pos.Y - radius; y <= centerGrid.Pos.Y + radius; y++) { if (x == centerGrid.Pos.X && y == centerGrid.Pos.Y) continue; var distance = Mathf.Max(Mathf.Abs(x - centerGrid.Pos.X), Mathf.Abs(y - centerGrid.Pos.Y)); if (distance > radius) continue; if (!mapData.GridMap.GetGridDataByPos(x, y, out var grid)) continue; if (!CanPlaceSeaStartResource(grid)) continue; var isPreferred = distance >= preferredMinDistance; if (grid.Resource == ResourceType.None) { if (isPreferred) preferredNone.Add(grid); else fallbackNone.Add(grid); } else if (allowReplaceFish && resource != ResourceType.Fish && grid.Resource == ResourceType.Fish) { if (isPreferred) preferredFish.Add(grid); else fallbackFish.Add(grid); } } var placed = 0; placed += PlaceSeaStartResourceInCandidates(preferredNone, resource, addCount - placed); placed += PlaceSeaStartResourceInCandidates(fallbackNone, resource, addCount - placed); placed += PlaceSeaStartResourceInCandidates(preferredFish, resource, addCount - placed); placed += PlaceSeaStartResourceInCandidates(fallbackFish, resource, addCount - placed); return placed; } private bool CanPlaceSeaStartResource(GridData grid) { if (grid == null) return false; if (grid.Terrain == TerrainType.Land) return false; if (grid.Resource is ResourceType.Tower or ResourceType.CityCenter or ResourceType.Treasure) return false; return true; } private int PlaceSeaStartResourceInCandidates(List candidates, ResourceType resource, int maxCount) { if (maxCount <= 0 || candidates.Count == 0) return 0; ShuffleList(candidates); var placed = 0; foreach (var grid in candidates) { if (placed >= maxCount) break; if (grid.Resource == resource) continue; grid.Resource = resource; placed++; } return placed; } private bool HasTower(int x, int y) { if (x == 0 && y == 0) return true; if (x == 0 && y == _height - 1) return true; if (x == _width - 1 && y == 0) return true; if (x == _width - 1 && y == _height - 1) return true; return false; } //生成文明摇篮城市(每个玩家的原始首都) private void GenerateCradleCity(MapData mapData) { //制作文明摇篮地点候选列表 var cityIndices = new System.Collections.Generic.List(); foreach (var c in _tribes) { var nearbyLand = 0; var cx = (int)c.PosId / 1000; var cy = (int)c.PosId % 1000; for (int x = cx - 1; x <= cx + 1; x++) for (int y = cy - 1; y <= cy + 1; y++) if (x >= 0 && x < _width && y >= 0 && y < _height && _heightMap[x, y] > _landThreshold) nearbyLand++; if (nearbyLand > 0) cityIndices.Add(new MapPosition(cx,cy)); } if (cityIndices.Count < mapData.MapConfig.PlayerCount) { mapData.MapConfig.PlayerCount = (uint)cityIndices.Count; // 移除多余的玩家,保留与可用出生点数量一致的玩家 mapData.PlayerMap.TrimPlayerCount(cityIndices.Count); // 重新初始化结算信息,使其与裁剪后的玩家列表一致 mapData.MatchSettlement.Init(mapData, mapData.MapConfig); } PlayerCivOri = GeneratePlayerCivOrigins(mapData, cityIndices); //建立玩家档案,建立具体城市,建立初始单位 int rk = 0; foreach (var p in mapData.PlayerMap.PlayerDataList) { GridData g; mapData.GridMap.GetGridDataByPos(PlayerCivOri[rk].X, PlayerCivOri[rk].Y, out g); //建设一个新城市 CityData c = mapData.AddCityData(g.Id, p.Id); //如果玩家拥有七河之地科技,初始城市等级为4 if (p.TechTree.CheckIfHasTechAtom(TechAtom.KomeijiIndianSevenRiver)) c.Level = 4; //设置该城市为首都 Main.CityLogic.SetCapital(mapData, c.Id); //设置该玩家的文明摇篮城市为该城市 p.CradleCityId = c.Id; //Main.CityLogic.SetCradle(mapData, c.Id); //TODO grid还没初始化好,这里不应该先建立unit的,应该在grid初始化结束后建单位 //建立文明的开国单位,并额外赋予初始行动点 if(mapData.AddUnitData(g.Id,c.Id,new UnitFullType(UnitType.Warrior,GiantType.None,0),out var newUnit)) newUnit.SetFullActionPoint(); rk++; } } private List GeneratePlayerCivOrigins(MapData mapData, List cityIndices) { var playerCount = mapData.PlayerMap.PlayerDataList.Count; var origins = new List(playerCount); for (int i = 0; i < playerCount; i++) origins.Add(null); var available = new List(cityIndices); var realPlayerIndices = new List(); for (int i = 0; i < playerCount; i++) if (IsRealPlayerIndex(mapData, i)) realPlayerIndices.Add(i); ShuffleList(realPlayerIndices); foreach (var realPlayerIndex in realPlayerIndices) { if (UnityEngine.Random.Range(0f, 1f) >= NearbyEnemyCapitalRateForRealPlayer) continue; TryAssignNearbyEnemyCapital(mapData, origins, available, realPlayerIndex); } FillRemainingPlayerCivOrigins(origins, available); return origins; } private bool TryAssignNearbyEnemyCapital( MapData mapData, List origins, List available, int realPlayerIndex) { if (realPlayerIndex < 0 || realPlayerIndex >= origins.Count) return false; var realOrigin = origins[realPlayerIndex]; if (realOrigin == null) { var realCandidates = new List(); foreach (var candidate in available) if (HasNearbyEnemyCandidate(mapData, origins, available, realPlayerIndex, candidate)) realCandidates.Add(candidate); if (realCandidates.Count == 0) return false; realOrigin = SelectBestOriginCandidate(realCandidates, origins); AssignPlayerCivOrigin(origins, available, realPlayerIndex, realOrigin); } if (HasAssignedNearbyEnemy(mapData, origins, realPlayerIndex, realOrigin)) return true; var enemyIndices = new List(); for (int i = 0; i < origins.Count; i++) if (origins[i] == null && IsEnemyPlayerIndex(mapData, realPlayerIndex, i)) enemyIndices.Add(i); if (enemyIndices.Count == 0) return false; var enemyCandidates = new List(); foreach (var candidate in available) if (ChebyshevDistance(realOrigin, candidate) == NearbyEnemyCapitalDistance) enemyCandidates.Add(candidate); if (enemyCandidates.Count == 0) return false; var enemyIndex = enemyIndices[UnityEngine.Random.Range(0, enemyIndices.Count)]; var enemyOrigin = enemyCandidates[UnityEngine.Random.Range(0, enemyCandidates.Count)]; AssignPlayerCivOrigin(origins, available, enemyIndex, enemyOrigin); return true; } private bool HasNearbyEnemyCandidate( MapData mapData, List origins, List available, int realPlayerIndex, MapPosition realOrigin) { for (int i = 0; i < origins.Count; i++) { if (!IsEnemyPlayerIndex(mapData, realPlayerIndex, i)) continue; if (origins[i] != null) { if (ChebyshevDistance(realOrigin, origins[i]) == NearbyEnemyCapitalDistance) return true; continue; } foreach (var candidate in available) if (candidate != realOrigin && ChebyshevDistance(realOrigin, candidate) == NearbyEnemyCapitalDistance) return true; } return false; } private bool HasAssignedNearbyEnemy( MapData mapData, List origins, int realPlayerIndex, MapPosition realOrigin) { for (int i = 0; i < origins.Count; i++) { if (origins[i] == null) continue; if (!IsEnemyPlayerIndex(mapData, realPlayerIndex, i)) continue; if (ChebyshevDistance(realOrigin, origins[i]) == NearbyEnemyCapitalDistance) return true; } return false; } private void FillRemainingPlayerCivOrigins(List origins, List available) { for (int i = 0; i < origins.Count; i++) { if (origins[i] != null) continue; if (available.Count == 0) break; var origin = SelectBestOriginCandidate(available, origins); AssignPlayerCivOrigin(origins, available, i, origin); } } private void AssignPlayerCivOrigin( List origins, List available, int playerIndex, MapPosition origin) { origins[playerIndex] = origin; available.Remove(origin); } private bool IsRealPlayerIndex(MapData mapData, int playerIndex) { if (mapData?.PlayerMap?.PlayerDataList == null || playerIndex < 0 || playerIndex >= mapData.PlayerMap.PlayerDataList.Count) return false; if (mapData.Net?.Mode == NetMode.Multi) { var slot = mapData.MapConfig?.MultiCivs != null && playerIndex < mapData.MapConfig.MultiCivs.Count ? mapData.MapConfig.MultiCivs[playerIndex] : null; return slot != null && slot.MemberId != 0 && !slot.IsAI; } return playerIndex == 0 || mapData.PlayerMap.PlayerDataList[playerIndex].Id == mapData.PlayerMap.SelfPlayerId; } private bool IsEnemyPlayerIndex(MapData mapData, int selfIndex, int targetIndex) { if (selfIndex == targetIndex) return false; if (mapData?.PlayerMap?.PlayerDataList == null) return false; if (selfIndex < 0 || selfIndex >= mapData.PlayerMap.PlayerDataList.Count) return false; if (targetIndex < 0 || targetIndex >= mapData.PlayerMap.PlayerDataList.Count) return false; var selfPlayer = mapData.PlayerMap.PlayerDataList[selfIndex]; var targetPlayer = mapData.PlayerMap.PlayerDataList[targetIndex]; return !mapData.MapConfig.ArePlayersInSameTeam(selfPlayer.Id, targetPlayer.Id); } private MapPosition SelectBestOriginCandidate(List candidates, List origins) { var hasSeed = false; foreach (var origin in origins) { if (origin == null) continue; hasSeed = true; break; } if (!hasSeed) return FarthestPointSampling(candidates, 1)[0]; var bestScore = -1f; var bestCandidates = new List(); foreach (var candidate in candidates) { var minDist = float.MaxValue; foreach (var origin in origins) { if (origin == null) continue; var dx = candidate.X - origin.X; var dy = candidate.Y - origin.Y; var dist = dx * dx + dy * dy; if (dist < minDist) minDist = dist; } var score = minDist * GetEdgeWeight(candidate); if (score > bestScore) { bestScore = score; bestCandidates.Clear(); bestCandidates.Add(candidate); } else if (Mathf.Approximately(score, bestScore)) { bestCandidates.Add(candidate); } } return bestCandidates[UnityEngine.Random.Range(0, bestCandidates.Count)]; } private float GetEdgeWeight(MapPosition pos) { float cx = (_width - 1) / 2f; float cy = (_height - 1) / 2f; float distToEdge = Mathf.Min( Mathf.Min(pos.X, _width - 1 - pos.X), Mathf.Min(pos.Y, _height - 1 - pos.Y) ); float halfSize = Mathf.Min(cx, cy); float ratio = halfSize > 0 ? Mathf.Clamp01(distToEdge / halfSize) : 1f; return 0.3f + 0.7f * ratio; } private int ChebyshevDistance(MapPosition a, MapPosition b) { return Mathf.Max(Mathf.Abs(a.X - b.X), Mathf.Abs(a.Y - b.Y)); } private void ShuffleList(List list) { for (int i = list.Count - 1; i > 0; i--) { int j = UnityEngine.Random.Range(0, i + 1); (list[i], list[j]) = (list[j], list[i]); } } //返回x,y位置的海陆层信息 private TerrainType GenerateTerrain(int x, int y,CivEnum civ = CivEnum.Common,InnerOuterCity innerOuterCity = InnerOuterCity.All) { // 走柏林噪音判断 var heightValue = _heightMap[x, y]; if (heightValue > _landThreshold) return TerrainType.Land; if (x + 1 < _width && _heightMap[x + 1, y] > _landThreshold) return TerrainType.ShallowSea; if (x - 1 > 0 && _heightMap[x - 1, y] > _landThreshold) return TerrainType.ShallowSea; if (y + 1 < _height && _heightMap[x, y + 1] > _landThreshold) return TerrainType.ShallowSea; if (y - 1 > 0 && _heightMap[x, y - 1] > _landThreshold) return TerrainType.ShallowSea; return TerrainType.DeepSea; } //返回x,y位置的地形层信息 private TerrainFeature GenerateFeature(int x, int y,CivEnum civ = CivEnum.Common,InnerOuterCity innerOuterCity = InnerOuterCity.All) { if (_tribes.Contains(new MapPosition(x, y))) return TerrainFeature.None;//如果是城市中心那必没有山 var table = Table.Instance.PlayerDataAssets; return UnityEngine.Random.Range(0f, 1f) < table.GetLandformRate(TerrainFeature.Mountain, civ) ? TerrainFeature.Mountain : TerrainFeature.None; } //返回x,y位置的植被层信息 private Vegetation GenerateVegetation(int x, int y,CivEnum civ = CivEnum.Common,InnerOuterCity innerOuterCity = InnerOuterCity.All) { if (_tribes.Contains(new MapPosition(x,y))) return Vegetation.None;//如果是城市中心那必没有树 var table = Table.Instance.PlayerDataAssets; var a = UnityEngine.Random.Range(0f, 1f); var b = table.GetLandformRate(Vegetation.Trees, civ); //Debug.Log($"{x},{y},{civ},{innerOuterCity} : {a}/{b}, Tree"); return a < b ? Vegetation.Trees : Vegetation.None; } //返回x,y位置的资源层信息 private ResourceType GenerateResource(int x, int y, TerrainType terrain, TerrainFeature feature, Vegetation vegetation,CivEnum civ = CivEnum.Common,InnerOuterCity innerOuterCity = InnerOuterCity.All) { if (_tribes.Contains(new MapPosition(x,y))) return ResourceType.CityCenter; var table = Table.Instance.PlayerDataAssets; //Step #1 不再二环以内的,只能考虑遗迹和海星,且不能有其他资源 if (innerOuterCity == InnerOuterCity.Edge) { //先考虑遗迹 if (UnityEngine.Random.Range(0f, 1f) < table.GetLandformRate(ResourceType.Treasure, civ, innerOuterCity)) { //如果遗迹附近有别的遗迹,不连着生成 if (_treasure.Any(P => Math.Abs(P.X - x) + Math.Abs(P.Y - y) <= 2)) return ResourceType.None; _treasure.Add(new MapPosition(x,y)); return ResourceType.Treasure; } //再考虑鲸鱼 if (terrain != TerrainType.Land) { if (UnityEngine.Random.Range(0f, 1f) < table.GetLandformRate(ResourceType.Starfish, civ, innerOuterCity)) return ResourceType.Starfish; } return ResourceType.None; } //Step #2 如果是海洋 if (terrain != TerrainType.Land) { //先考虑渔业,再考虑鲸鱼,最后考虑Treasure if (UnityEngine.Random.Range(0f, 1f) < table.GetLandformRate(ResourceType.Fish, civ, innerOuterCity)) return ResourceType.Fish; if (UnityEngine.Random.Range(0f, 1f) < table.GetLandformRate(ResourceType.Starfish, civ, innerOuterCity)) return ResourceType.Starfish; if (UnityEngine.Random.Range(0f, 1f) < table.GetLandformRate(ResourceType.Treasure, civ, innerOuterCity)) { //如果遗迹附近有别的遗迹,不连着生成 if (_treasure.Any(P => Math.Abs(P.X - x) + Math.Abs(P.Y - y) <= 2)) return ResourceType.None; _treasure.Add(new MapPosition(x,y)); return ResourceType.Treasure; } return ResourceType.None; } //Step #3 如果是陆地先处理山,再处理树,最后处理Fruit和Crop if (feature == TerrainFeature.Mountain) { var a = UnityEngine.Random.Range(0f, 1f); var b = table.GetLandformRate(ResourceType.Metal, civ, innerOuterCity); //Debug.Log($"{x},{y},{a}/{b},Metal"); //return UnityEngine.Random.Range(0f, 1f) < table.GetLandformRate(ResourceType.Metal, civ, innerOuterCity) return a < b ? ResourceType.Metal : ResourceType.None; } if (vegetation == Vegetation.Trees) { var a = UnityEngine.Random.Range(0f, 1f); var b = table.GetLandformRate(ResourceType.Animal, civ, innerOuterCity); //Debug.Log($"{x},{y},{a}/{b},Animal"); return a < b ? ResourceType.Animal : ResourceType.None; } if (UnityEngine.Random.Range(0f, 1f) < table.GetLandformRate(ResourceType.Fruit, civ, innerOuterCity)) return ResourceType.Fruit; if (UnityEngine.Random.Range(0f, 1f) < table.GetLandformRate(ResourceType.Crop, civ, innerOuterCity)) return ResourceType.Crop; if (UnityEngine.Random.Range(0f, 1f) < table.GetLandformRate(ResourceType.Treasure, civ, innerOuterCity)) { //如果遗迹附近有别的遗迹,不连着生成 if (_treasure.Any(P => Math.Abs(P.X - x) + Math.Abs(P.Y - y) <= 2)) return ResourceType.None; _treasure.Add(new MapPosition(x,y)); return ResourceType.Treasure; } return ResourceType.None; } //返回x,y位置的文化层信息 private uint GenerateCivilization(MapData mapData,int x, int y) { // 简单的随机分配一个文明,或者使用更复杂的算法进行分布 var minDis = 2139062143; var civSeed = -1; var tt = 0; //Debug.Log(PlayerCivOri.Count); foreach (var p in PlayerCivOri) { if (Mathf.Abs(p.X - x) + Mathf.Abs(p.Y - y) < minDis) { minDis = (int)(Mathf.Abs(p.X - x) + Mathf.Abs(p.Y - y)); civSeed = (int)_mapData.PlayerMap.PlayerDataList[tt].PlayerCivId; } else if ((Mathf.Abs(p.X - x) + Mathf.Abs(p.Y - y) == minDis) && UnityEngine.Random.Range(0, 2) == 1) { minDis = (int)(Mathf.Abs(p.X - x) + Mathf.Abs(p.Y - y)); civSeed = (int)_mapData.PlayerMap.PlayerDataList[tt].PlayerCivId; } else if ((Mathf.Abs(p.X - x) + Mathf.Abs(p.Y - y) == minDis + 1) && UnityEngine.Random.Range(0, 3) == 1) { minDis = (int)(Mathf.Abs(p.X - x) + Mathf.Abs(p.Y - y)); civSeed = (int)_mapData.PlayerMap.PlayerDataList[tt].PlayerCivId; } tt++; } if (civSeed == -1) return (uint)UnityEngine.Random.Range(1, mapData.MapConfig.PlayerCount + 1); return (uint)civSeed; } //初始化所有玩家的视野信息 private void InitAllPlayerSight(MapData mapData) { foreach (var cityData in mapData.CityMap.CityList) { mapData.GetPlayerDataByCityId(cityData.Id, out var playerData); mapData.GetGridDataByCityId(cityData.Id, out var gridData); Main.PlayerLogic.UpdateSight_LogicView(mapData, playerData, mapData.GridMap.GetAroundGridIdList(2,gridData,true),false,0.01f); } } private void InitDebugInfo(MapData mapData) { if (DebugCenter.Instance.DebugSelfPlayerAllSight) { System.Collections.Generic.List tt = new System.Collections.Generic.List(); foreach (var gridData in mapData.GridMap.GridList) tt.Add(gridData.Id); Main.PlayerLogic.UpdateSight_LogicView(mapData,mapData.PlayerMap.SelfPlayerData,tt); } if (DebugCenter.Instance.DebugAIAllTech) { foreach (var playerData in mapData.PlayerMap.PlayerDataList) { if (playerData == mapData.PlayerMap.SelfPlayerData) continue; playerData.TechTree.LearnTech(TechType.Fishing, playerData); playerData.TechTree.LearnTech(TechType.Organization, playerData); playerData.TechTree.LearnTech(TechType.Farming, playerData); playerData.TechTree.LearnTech(TechType.Mining, playerData); playerData.TechTree.LearnTech(TechType.Forestry, playerData); playerData.TechTree.LearnTech(TechType.Hunting, playerData); playerData.TechTree.LearnTech(TechType.Smithery, playerData); playerData.TechTree.LearnTech(TechType.Mathematics, playerData); playerData.TechTree.LearnTech(TechType.FreeSpirit, playerData); playerData.TechTree.LearnTech(TechType.Chivalry, playerData); playerData.TechTree.LearnTech(TechType.Sailing, playerData); playerData.TechTree.LearnTech(TechType.Navigation, playerData); playerData.TechTree.LearnTech(TechType.Ramming, playerData); playerData.TechTree.LearnTech(TechType.Riding, playerData); playerData.TechTree.LearnTech(TechType.Archery, playerData); playerData.TechTree.LearnTech(TechType.Spiritualism, playerData); playerData.TechTree.LearnTech(TechType.Construction, playerData); playerData.TechTree.LearnTech(TechType.Meditation, playerData); playerData.TechTree.LearnTech(TechType.Aquatism, playerData); } } if (DebugCenter.Instance.DebugAIMoreMoney) { foreach (var playerData in mapData.PlayerMap.PlayerDataList) { if (playerData == mapData.PlayerMap.SelfPlayerData) continue; playerData.AddCoin(10); } } } //初始化所有玩家的特殊领地技能(目前主要是kaguya) private void InitPlayerSpecialSkill(MapData mapData) { //遍历每一个玩家,处理每一种可能得specialSkill foreach (var player in mapData.PlayerMap.PlayerDataList) { //首先通过Tech 赋予玩家技能 //处理kaguya专属skill if (player.TechTree.CheckIfHasTechAtom(TechAtom.KaguyaFrenchNapoleonicCode)) { //设置为永远亭领地 var cityList = new List(); mapData.GetCityDataListByPlayerId(player.Id,cityList); foreach (var city in cityList) { Main.PlayerLogic.SetCityTerritoryGridSp(mapData,city,GridSpType.KaguyaGrid); //获得首都4格内的遗迹视野 if(city.Id !=player.CradleCityId)continue; _aroundBuf ??= new List(); _aroundBuf.Clear(); mapData.GridMap.GetAroundGridData(4, 4, city.Grid(mapData), _aroundBuf); var gidList = new List(); foreach(var grid in _aroundBuf) if(grid.Resource == ResourceType.Treasure) gidList.Add(grid.Id); Main.PlayerLogic.UpdateSight_LogicView(mapData, player, gidList); } } } } /// /// 最远点采样:从候选列表中选出count个点,使得它们之间的最小距离尽量大。 /// 先随机选一个起始点,之后每轮选择离已选点集最远的候选点。 /// private List FarthestPointSampling(List candidates, int count) { if (candidates.Count <= count) return new List(candidates); // 预计算每个候选点的边缘权重:越靠近地图边缘,权重越低,避免总是选到角落 float cx = (_width - 1) / 2f; float cy = (_height - 1) / 2f; var edgeWeight = new float[candidates.Count]; for (int i = 0; i < candidates.Count; i++) { // 计算到四条边的最小距离占半幅的比例 [0,1] float distToEdge = Mathf.Min( Mathf.Min(candidates[i].X, _width - 1 - candidates[i].X), Mathf.Min(candidates[i].Y, _height - 1 - candidates[i].Y) ); float halfSize = Mathf.Min(cx, cy); float ratio = halfSize > 0 ? Mathf.Clamp01(distToEdge / halfSize) : 1f; // 权重范围 [0.3, 1.0]:最边缘处仍有0.3的权重,不会完全排除 edgeWeight[i] = 0.3f + 0.7f * ratio; } var selected = new List(); // 第一个点:按边缘权重加权随机选取,偏好远离边缘的位置 float totalWeight = 0f; for (int i = 0; i < candidates.Count; i++) totalWeight += edgeWeight[i]; float roll = Random.Range(0f, totalWeight); int firstIdx = 0; float acc = 0f; for (int i = 0; i < candidates.Count; i++) { acc += edgeWeight[i]; if (acc >= roll) { firstIdx = i; break; } } selected.Add(candidates[firstIdx]); // minDist[i] = 候选点i到已选点集的最小距离 var minDist = new float[candidates.Count]; for (int i = 0; i < candidates.Count; i++) minDist[i] = float.MaxValue; for (int sel = 1; sel < count; sel++) { // 用上一轮新选入的点更新所有候选点的最小距离 var lastSelected = selected[sel - 1]; for (int i = 0; i < candidates.Count; i++) { float dx = candidates[i].X - lastSelected.X; float dy = candidates[i].Y - lastSelected.Y; float dist = dx * dx + dy * dy; // 用平方距离即可,无需开根号 if (dist < minDist[i]) minDist[i] = dist; } // 选择加权后 minDist 最大的候选点(兼顾远离已选点 + 远离边缘) int bestIdx = -1; float bestScore = -1f; for (int i = 0; i < candidates.Count; i++) { float score = minDist[i] * edgeWeight[i]; if (score > bestScore) { bestScore = score; bestIdx = i; } } selected.Add(candidates[bestIdx]); minDist[bestIdx] = 0f; // 标记已选 } return selected; } private void GenerateLeyLines(MapData mapData) { // Step #1 计算目标数量:地脉总数 = 城市中心总数 / 3 int leyLineCount = _tribes.Count / 3; bool hasIndianPlayer = false; foreach (var player in mapData.PlayerMap.PlayerDataList) if (player.CivEnum == CivEnum.Indian) { hasIndianPlayer = true; break; } if (leyLineCount <= 0 && !hasIndianPlayer) return; // Step #2 构建首都排除区(仅首都±1范围,非首都村庄不排除) using var pooledCradleExclude = THCollectionPool.GetHashSetHandle(out var cradleExclude); foreach (var cradle in PlayerCivOri) for (int x = cradle.X - 1; x <= cradle.X + 1; x++) for (int y = cradle.Y - 1; y <= cradle.Y + 1; y++) cradleExclude.Add(MapPosition.CalculatePosId(x, y)); // Step #3 收集候选格子:陆地 + 非CityCenter + 不在首都1格范围内 using var pooledCandidates = THCollectionPool.GetListHandle(out var candidates); foreach (var gridData in mapData.GridMap.GridList) { if (!CanPlaceLeyLine(gridData, cradleExclude)) continue; candidates.Add(gridData); } // Step #4 Fisher-Yates 随机打乱 for (int i = candidates.Count - 1; i > 0; i--) { int j = Random.Range(0, i + 1); (candidates[i], candidates[j]) = (candidates[j], candidates[i]); } // Step #5 取前N个,标记为地脉 int placed = 0; foreach (var grid in candidates) { if (placed >= leyLineCount) break; if (TryAddLeyLine(mapData, grid, cradleExclude, true)) placed++; } // Step #6 古明地帝国保底:首都2格内必定有地脉 for (int rk = 0; rk < mapData.PlayerMap.PlayerDataList.Count && rk < PlayerCivOri.Count; rk++) { var player = mapData.PlayerMap.PlayerDataList[rk]; if (player.CivEnum != CivEnum.Indian) continue; if (!mapData.GridMap.GetGridDataByPos(PlayerCivOri[rk].X, PlayerCivOri[rk].Y, out var cradleGrid)) continue; // 检查2格内是否已有地脉 _aroundBuf ??= new List(); _aroundBuf.Clear(); mapData.GridMap.GetAroundGridData(2, 2, cradleGrid, _aroundBuf); bool hasLeyLine = false; foreach (var g in _aroundBuf) if (g.HasSpType(GridSpType.LeyLine) && CanPlaceLeyLine(g, cradleExclude)) { hasLeyLine = true; break; } if (hasLeyLine) continue; // 没有则强制在2格内随机选一个合法格子放置 bool placedNearCradle = TryPlaceLeyLineAround(mapData, cradleGrid, 2, cradleExclude, true); if (placedNearCradle) continue; int maxRadius = (int)(_width > _height ? _width : _height); bool placedByExpand = false; for (int radius = 3; radius <= maxRadius; radius++) { if (TryPlaceLeyLineAround(mapData, cradleGrid, radius, cradleExclude, true)) { placedByExpand = true; break; } } if (placedByExpand) continue; for (int radius = 2; radius <= maxRadius; radius++) if (TryPlaceLeyLineAround(mapData, cradleGrid, radius, cradleExclude, false)) break; } } private bool TryPlaceLeyLineAround(MapData mapData, GridData centerGrid, int radius, HashSet cradleExclude, bool avoidNearbyLeyLine) { _aroundBuf ??= new List(); _aroundBuf.Clear(); mapData.GridMap.GetAroundGridData(radius, radius, centerGrid, _aroundBuf); for (int i = _aroundBuf.Count - 1; i > 0; i--) { int j = Random.Range(0, i + 1); (_aroundBuf[i], _aroundBuf[j]) = (_aroundBuf[j], _aroundBuf[i]); } foreach (var grid in _aroundBuf) { if (!CanPlaceLeyLine(grid, cradleExclude)) continue; if (TryAddLeyLine(mapData, grid, cradleExclude, avoidNearbyLeyLine)) return true; } return false; } private bool TryAddLeyLine(MapData mapData, GridData grid, HashSet cradleExclude, bool avoidNearbyLeyLine) { if (!CanPlaceLeyLine(grid, cradleExclude)) return false; if (grid.HasSpType(GridSpType.LeyLine)) return false; if (avoidNearbyLeyLine && HasNearbyLeyLine(mapData, grid, 1)) return false; return grid.AddSpType(GridSpType.LeyLine, mapData, null); } private bool HasNearbyLeyLine(MapData mapData, GridData grid, int radius) { for (int x = grid.Pos.X - radius; x <= grid.Pos.X + radius; x++) for (int y = grid.Pos.Y - radius; y <= grid.Pos.Y + radius; y++) { if (x == grid.Pos.X && y == grid.Pos.Y) continue; if (!mapData.GridMap.GetGridDataByPos(x, y, out var aroundGrid)) continue; if (aroundGrid.HasSpType(GridSpType.LeyLine)) return true; } return false; } private bool CanPlaceLeyLine(GridData grid, HashSet cradleExclude) { if (grid == null) return false; if (grid.Terrain != TerrainType.Land) return false; if (grid.Feature == TerrainFeature.Mountain) return false; if (grid.Resource == ResourceType.CityCenter) return false; if (IsMapEdgeGrid(grid)) return false; if (cradleExclude.Contains(grid.Pos.PosId)) return false; return true; } private bool IsMapEdgeGrid(GridData grid) { return grid.Pos.X <= 0 || grid.Pos.Y <= 0 || grid.Pos.X >= _width - 1 || grid.Pos.Y >= _height - 1; } } }