簡體   English   中英

在統一 2D C# 中循環通過大瓷磚地圖的性能問題

[英]Performance issue looping through big tilemap in unity 2D C#

我有一個瓦片地圖,它可以根據我存儲在 position 字典中的數據傳播到相鄰的瓦片。

水傳播

雖然檢查很快就完成了,但每次我調用 function 時,它確實需要相當多的資源導致滯后。

目前,我通過每次完成循環迭代時實際等待一點來確保代碼不會被執行得太快。 雖然我對我的代碼沒有盡可能快地執行感到滿意,但這並不是 go 關於它的正確方法。

所以基本上:有沒有辦法限制單一腳本/函數在unity / c#中的執行不使用超過一定百分比的游戲資源(或類似的東西)? 或者也許有一種方法可以顯着提高我找不到的功能性能?

編碼:

using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using System;
using UnityEngine.Tilemaps;
using Random = System.Random;

public class MapManager : MonoBehaviour
{
    [SerializeField] private Tilemap tilemap;
    [SerializeField] private List<TileType> tileTypes;
    [SerializeField] private float startTime, repeatTime, waterLevel, heightModifier, minimumFertility, maximumFertility;
    private List<Vector3Int> tilePositions;
    private HashSet<Vector3Int> tilePositionsOfChangedTiles;
    private Dictionary<Vector3Int, TileData> tileDataByPosition;
    private Dictionary<TileBase, TileType> tileTypeByTile;
    private static Random randomGenerator = new Random();
    private void Awake()
    {
        tileDataByPosition = new Dictionary<Vector3Int, TileData>();
        tileTypeByTile = new Dictionary<TileBase, TileType>();
        tilePositionsOfChangedTiles = new HashSet<Vector3Int>();
        foreach (TileType tileType in tileTypes) tileTypeByTile.Add(tileType.tile, tileType);
        tilePositions = new List<Vector3Int>();
        for (int n = tilemap.cellBounds.xMin; n < tilemap.cellBounds.xMax; n++)
        {
            for (int p = tilemap.cellBounds.yMin; p < tilemap.cellBounds.yMax; p++)
            {
                Vector3Int localPlace = (new Vector3Int(n, p, 0));
                if (tilemap.HasTile(localPlace))
                {
                    TileBase tile = tilemap.GetTile(localPlace);
                    TileType tileType = tileTypeByTile[tile];
                    tilePositions.Add(localPlace);
                    tileDataByPosition.Add(localPlace, new TileData(waterLevel, tileType.baseFertility, tileType.isSubmerged == true));
                    if (tileType.isSubmerged == true) tileDataByPosition[localPlace].Height += randomGenerator.Next(-(int)heightModifier, 0);
                    else tileDataByPosition[localPlace].Height += randomGenerator.Next(-(int)heightModifier, (int)heightModifier);
                }
            }
        }
        StartCoroutine(Spread());
    }
    private void UpdateTiles()
    {
        foreach (Vector3Int position in tilePositionsOfChangedTiles)
        {
            TileBase currentTile = tilemap.GetTile(position);
            if (currentTile == null) continue;
            TileType currentTileType = tileTypeByTile[currentTile];
            TileData currentTileData = tileDataByPosition[position];
            TileType chosenTileType = null;
            List<TileType> tileTypesToUse = tileTypes.Where(tileType => tileType.isSubmerged == currentTileData.Submerged).ToList();
            chosenTileType = tileTypesToUse.Aggregate((typeOne, typeTwo) =>
            {
                if (Math.Abs(typeOne.baseFertility - tileDataByPosition[position].Fertility) < Math.Abs(typeTwo.baseFertility - tileDataByPosition[position].Fertility)) return typeOne;
                else return typeTwo;
            });
            if (
                chosenTileType == null ||
                chosenTileType == currentTileType
            ) continue;
            tilemap.SetTile(position, chosenTileType.tile);
        }
        tilePositionsOfChangedTiles.Clear();
        StartCoroutine(Spread());
    }
    IEnumerator Spread()
    {
        foreach (Vector3Int position in tilePositions)
        {
            TileBase currentTile = tilemap.GetTile(position);
            if (currentTile == null) continue;
            TileType currentTileType = tileTypeByTile[currentTile];
            if (
                currentTileType.spreadFertility == false &&
                currentTileType.isSubmerged == false
            ) continue;
            AddFertility(position, currentTileType.fertilityModifier);
            tilePositionsOfChangedTiles.Add(position);
            List<Vector3Int> adjacentTilePositions = new List<Vector3Int>();
            TileData currentTileData = tileDataByPosition[position];
            if (currentTileData.Submerged == true)
            {
                adjacentTilePositions = GetAdjacentTiles(position, 1);
                foreach (Vector3Int adjacentTilePosition in adjacentTilePositions)
                {
                    TileData adjacentTileData = tileDataByPosition[adjacentTilePosition];
                    if (adjacentTileData.Height <= currentTileData.Height)
                    {
                        adjacentTileData.Submerged = true;
                        tilePositionsOfChangedTiles.Add(position);
                    }
                }
            }
            adjacentTilePositions = GetAdjacentTiles(position, (int)currentTileType.spreadRange);
            foreach (Vector3Int adjacentTilePosition in adjacentTilePositions)
            {
                AddFertility(adjacentTilePosition, currentTileType.fertilityModifier);
                tilePositionsOfChangedTiles.Add(position);
            }
            yield return new WaitForSeconds(0.001f);
        }
        UpdateTiles();
    }
    private void AddFertility(Vector3Int position, float fertility)
    {
        if (
            fertility <= minimumFertility ||
            fertility >= maximumFertility
        ) return;
        tileDataByPosition[position].Fertility = Mathf.Clamp(fertility + tileDataByPosition[position].Fertility, minimumFertility, maximumFertility);
        tilePositionsOfChangedTiles.Add(position);
    }
    private List<Vector3Int> GetAdjacentTiles(Vector3Int position, int radius)
    {
        List<Vector3Int> adjacentTilePositions = new List<Vector3Int>();
        for (int x = -radius; x <= radius; x++)
        {
            for (int y = -radius; y <= radius; y++)
            {
                if (Mathf.Abs(x) + Mathf.Abs(y) <= radius)
                {
                    Vector3Int positionToTry = new Vector3Int(position.x + x, position.y + y, position.z);
                    if (
                        positionToTry != position &&
                        tilePositions.Contains(positionToTry) == true
                    ) adjacentTilePositions.Add(positionToTry);
                }
            }
        }
        return adjacentTilePositions;
    }
}

提前致謝!

我建議將數據保存在多維數組中,並調用您需要的瓷磚地圖的特定區域。

示例偽代碼

TileData[,] tileMapDatas;

void Start()
{
    int rows = 5;
    int columns = 5;
    
    tileMapDatas = new TileData[rows, columns];
    
    for(int r = 0; r < rows, r++)
    {
        for(int c = 0; c < columns, c++)
        {
            tileMapDatas[r, c] = new TileData();//Change to how you create them
        }
    }
}

void Spread(Tile tile)
{
    if(tile.CanSpread)
    {
        //Check the mapdata for the tile above, (add check for array bounds)
        if(tileMapDatas[tile.Row + 1, tile.Column].CanChange)
        {
            //Change
        }
        
        //Repeat for other directions
    }
}

使用此方法效率更高,因為您只需要檢查目標圖塊的鄰居。

你想要做的是讓這個循環只執行每個循環總項目的一小部分,並更頻繁地調用它。 您可以分塊處理它。

我沒有專門檢查您的代碼以確切了解正在執行的操作,但這是隨着時間的推移拆分處理的常用方法,而不是您如何在協程中添加一個微小的 WaitForSeconds() 。 具有如此小的 WaitForSeconds 的協程可能對性能很不利,簡單地使用 Update() (每幀調用一次)並按照我描述的方式拆分處理將產生更好的性能。 如果你在谷歌上搜索協程性能,你可以找到相關的信息和基准。

您將創建一個變量來跟蹤索引的當前 position,以及一個變量,該變量是每幀要處理的最大索引數 - 當處理的項目總數達到最大值時,您將跳出循環。 您也不會執行從 0 到 X 的循環 - 它將從 currentIndex 到結尾,然后通過將其設置為 0 來循環。每次達到最大值時,您就會爆發。

總結一下:

  1. 從 Spread() 中刪除 IEnumerator 並使其成為常規 function,而不是 foreach(position in tilePosition) 使用 for(int i=0;) 等
  2. 添加一個像 MaxToProcessPerFrame 這樣的變量
  3. 添加另一個變量,例如 currentIndex
  4. 添加第三個變量,例如 totalProcessedThisFrame ,每次輸入 Spread() 時設置為零
  5. 例如 if (totalProcessedThisFrame++ >= MaxToProcessPerFrame) 中斷; 在你的循環中
  6. 添加 if (currentIndex >= tilePositions.Count) currentIndex = 0 以便循環回到開頭

我還注意到,Spread() 在最后調用了 UpdateTiles(),然后它會執行另一個 StartCoroutine(Spread()),這將導致它運行兩次,我確信在某種程度上是低效的。

調用 UpdateTiles() 的一個好地方可能是在 if (currentIndex >= tilePositions.Count) 上,因為那將是在更新整個列表的一次迭代結束時。

完成該設置后,您可以測試 MaxToProcessPerFrame 的不同值,從一個較大的數字開始,然后將其降低,直到它看起來運行平穩,並且它將打破多個幀的處理。

除了將要處理的項目總數分解之外,第二種方法是使用 System.Diagnostics Stopwatch() 並在循環超過處理的總毫秒數時中斷循環。

一旦你玩不同的數量,這兩種方法將有助於它順利處理。

最后,您不斷調用 StartCoroutine() 的方法也不高效。 您應該啟動一個協程一次或所需的最少次數,然后在其中循環。 通常使用 while (Application.isRunning) {} 循環作為 Unity 中大多數協程的基礎。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM