I have a tilemap with tiles that can spread to the adjacent tiles based on data I store in a dictionary by position.
While the check for that is done in no time at all it does take considerable resources resulting in a lag everytime I call the function.
Currently I'm making sure the code doesn't get executed too fast by actually waiting a little each time we finished a loop-iteration. While I'm okay with my code not being executed as fast as it could, this just isn't the right way to go about it.
So basically: Is there a way to limit the execution of a single script/function in unity/c# to not use more than a certain percentage of the games resources (or something to that effect)? Or maybe there's a way to increase the functions performance significantly I just can't find?
The code:
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;
}
}
Thanks in advance!
CodePudding user response:
I suggest keeping the data in a Multidimensional Array and call the specific area of the tilemap you need.
Example pseudo code
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
}
}
Using this method is a lot more efficient as you only need to check the neighbours of the targeted tile.
CodePudding user response:
To answer your: "Is there a way to limit the execution of a single script/function in unity/c# to not use more than a certain percentage of the games resources (or something to that effect)?" - What you want to do is have this loop only perform a fraction of the total items per loop and call it more often. You can process it in chunks or you can use time measurement as I noted at the end of this post.
I didn't examine your code specifically to see exactly what is being done, but this is a common way of splitting up the processing over time rather than how you were adding a tiny WaitForSeconds() within a Coroutine. The coroutine with such a tiny WaitForSeconds is probably terrible for performance and simply using Update() (which is called every frame) and splitting processing the way I describe will yield much better performance. If you google Coroutine performance you can find information and benchmarks on that.
You will create a variable to keep track of the current position of your index, and a variable that is the maximum number of indexes to process per frame - you will break out of your loop when the total # of items processed hits the max. You also will not do the loop from 0 to X - it will be from currentIndex to the end and then loop back around by setting it to 0. You will just break out when you hit max each time.
To summarize:
- Remove IEnumerator from Spread() and make it a regular function, and instead of foreach(position in tilePosition) use for(int i=0;) etc
- Add a variable like MaxToProcessPerFrame
- Add another variable like currentIndex
- Add a third variable like totalProcessedThisFrame that is set to zero each time you enter Spread()
- for instance if (totalProcessedThisFrame >= MaxToProcessPerFrame) break; in your loop
- add if (currentIndex >= tilePositions.Count) currentIndex = 0 so that it loops back around to the beginning
I also noticed that Spread() is calling UpdateTiles() at the end, which then does another StartCoroutine(Spread()) which will cause it to run twice and i'm sure is inefficient in some way since it will kind of double process possibly per iteration.
A likely good place to call UpdateTiles() would be on the if (currentIndex >= tilePositions.Count) since that would be at the end of one iteration of updating the whole list.
Once you have that setup, you can test different values for the MaxToProcessPerFrame, start with a high number and then lower it until it seems to run smoothly and it will break up the processing over multiple frames.
A second method in addition to breaking it up by a total # of items to be processed is to use a System.Diagnostics Stopwatch() and break out of your loop when it exceeds a total amount of milliseconds of processing.
These two methods will help it process smoothly once you play with different amounts.
And lastly, your method of calling StartCoroutine() constantly is also not performant. You should start a coroutine once or the least number of times needed, and then loop within it. It's common to use a while (Application.isRunning) {} loop as a basis for most Coroutines in Unity.
also for reference: Unity 2017 Game Optimization: Optimize all aspects of Unity performance By Chris Dickinson