Updated plant behavior

V1- Don't like how the variables are interacting. Will be rewriting plant behavior.
This commit is contained in:
2025-08-09 20:31:25 -07:00
parent 0c0af9e6a6
commit 6632f02d1c
6 changed files with 282 additions and 72 deletions
+8
View File
@@ -16,6 +16,9 @@
*.blend1 *.blend1
*.blend1.meta *.blend1.meta
# Imported assets
.Assets/Samples/
# MemoryCaptures can get excessive in size. # MemoryCaptures can get excessive in size.
# They also could contain extremely sensitive data # They also could contain extremely sensitive data
/[Mm]emoryCaptures/ /[Mm]emoryCaptures/
@@ -97,3 +100,8 @@ InitTestScene*.unity*
# Auto-generated scenes by play mode tests # Auto-generated scenes by play mode tests
/[Aa]ssets/[Ii]nit[Tt]est[Ss]cene*.unity* /[Aa]ssets/[Ii]nit[Tt]est[Ss]cene*.unity*
Assets/Samples/
Assets/TextMesh Pro/
*.meta
Assets/GameAssets/Materials/TerrainMaterial.mat
Assets/GameAssets/Materials/MaybePlants.shadergraph
+11 -2
View File
@@ -6,6 +6,9 @@ public class Core : MonoBehaviour
[SerializeField] public NutrientController nutrientCont; [SerializeField] public NutrientController nutrientCont;
[SerializeField] public PlantController plantCont; [SerializeField] public PlantController plantCont;
[SerializeField] public Terrain ground; [SerializeField] public Terrain ground;
[SerializeField, Range(0, 10)] public float speedOfStep;
float timeSoFar;
public static Core instance; public static Core instance;
@@ -17,12 +20,18 @@ public class Core : MonoBehaviour
// Start is called once before the first execution of Update after the MonoBehaviour is created // Start is called once before the first execution of Update after the MonoBehaviour is created
void Start() void Start()
{ {
timeSoFar = 0;
} }
// Update is called once per frame // Update is called once per frame
void Update() void Update()
{ {
timeSoFar += Time.deltaTime;
if(timeSoFar >= speedOfStep)
{
plantCont.Step(timeSoFar);
nutrientCont.Step(timeSoFar);
timeSoFar = 0;
}
} }
} }
+130 -39
View File
@@ -30,7 +30,21 @@ public class NutrientController : MonoBehaviour
} }
public bool PlantNutrientUse(float x, float y, float radius, float nutrientsNeeded) public void Step(float dTime)
{
for (int r = 0; r < sizeX; r++)
{
for (int c = 0; c < sizeY; c++)
{
if (nutrientMap[r,c] < 1)
{
nutrientMap[r, c] += (0.1f * dTime);
}
}
}
}
public float PlantNutrientUse(float x, float y, float radius, float nutrientsNeeded)
{ {
int locX = Mathf.FloorToInt(x * sizeX); int locX = Mathf.FloorToInt(x * sizeX);
int locY = Mathf.FloorToInt(y * sizeY); int locY = Mathf.FloorToInt(y * sizeY);
@@ -38,8 +52,18 @@ public class NutrientController : MonoBehaviour
int flooredRadius = Mathf.FloorToInt(radius); int flooredRadius = Mathf.FloorToInt(radius);
float totalNutrients = nutrientMap[locX, locY]; float totalNutrients = nutrientMap[locX, locY];
float[,] nutrientsAvailable = new float[flooredRadius * 2 + 1, flooredRadius * 2 + 1]; float[,] nutrientsAvailable;
try
{
nutrientsAvailable = new float[flooredRadius * 2 + 1, flooredRadius * 2 + 1];
} catch (Exception e)
{
Debug.Log($"x : {x} y : {y} radius : {radius} nutrients needed : {nutrientsNeeded}");
return 2000000;
}
// Calculate total amount of nutrients available to the plant, at a 1/ (2 * steps from origin) ratio.
for (int r = -flooredRadius; r <= flooredRadius; r++) for (int r = -flooredRadius; r <= flooredRadius; r++)
{ {
int currentLocX = locX + r; int currentLocX = locX + r;
@@ -47,75 +71,94 @@ public class NutrientController : MonoBehaviour
for(int c = -flooredRadius; c <= flooredRadius; c++) for(int c = -flooredRadius; c <= flooredRadius; c++)
{ {
int currentLocY = locY + c; int currentLocY = locY + c;
if (currentLocX < 0 || currentLocX >= nutrientMap.GetLength(0) || currentLocY < 0 || currentLocY >= nutrientMap.GetLength(1))
if(r == 0 && c == 0)
{ {
nutrientsAvailable[flooredRadius, flooredRadius] = nutrientMap[currentLocX, currentLocY]; nutrientsAvailable[flooredRadius, flooredRadius] = 0;
} }
else else
{ {
nutrientsAvailable[flooredRadius + r, flooredRadius + c] = nutrientMap[currentLocX, currentLocY] / ((Mathf.Abs(r) + Mathf.Abs(c)) * 2); if (r == 0 && c == 0)
totalNutrients += nutrientMap[currentLocX, currentLocY] / ((Mathf.Abs(r) + Mathf.Abs(c)) * 2); {
nutrientsAvailable[flooredRadius, flooredRadius] = nutrientMap[currentLocX, currentLocY];
}
else
{
nutrientsAvailable[flooredRadius + r, flooredRadius + c] = nutrientMap[currentLocX, currentLocY] / ((Mathf.Abs(r) + Mathf.Abs(c)) * 2);
totalNutrients += nutrientMap[currentLocX, currentLocY] / ((Mathf.Abs(r) + Mathf.Abs(c)) * 2);
}
} }
} }
} }
// If not enough nutrients are available to the plant to sustain it, return false.
// Can be changed to returning a float value indicating the deficit in nutrients with the plant taking that much in health damage
if (totalNutrients < nutrientsNeeded) if (totalNutrients < nutrientsNeeded)
{ {
return false; for (int r = -flooredRadius; r <= flooredRadius; r++)
{
int currentLocX = locX + r;
for (int c = -flooredRadius; c <= flooredRadius; c++)
{
int currentLocY = locY + c;
if (currentLocX >= 0 && currentLocX < nutrientMap.GetLength(0) && currentLocY >= 0 && currentLocY < nutrientMap.GetLength(1))
{
nutrientMap[currentLocX, currentLocY] -= (nutrientsAvailable[flooredRadius + r, flooredRadius + c]);
}
}
}
return nutrientsNeeded - totalNutrients;
} }
float[,] amountToUse = new float[flooredRadius * 2 + 1, flooredRadius * 2 + 1]; for (int r = -flooredRadius; r <= flooredRadius; r++)
for(int r = 0; r < nutrientsAvailable.GetLength(0); r++)
{ {
for (int c = 0; c < nutrientsAvailable.GetLength(1); c++) int currentLocX = locX + r;
for (int c = -flooredRadius; c <= flooredRadius; c++)
{ {
amountToUse[r, c] = (nutrientsAvailable[r, c] / totalNutrients) * nutrientsNeeded; int currentLocY = locY + c;
if (currentLocX >= 0 && currentLocX < nutrientMap.GetLength(0) && currentLocY >= 0 && currentLocY < nutrientMap.GetLength(1))
{
nutrientMap[currentLocX, currentLocY] -= (nutrientsAvailable[flooredRadius + r, flooredRadius + c] / totalNutrients) * nutrientsNeeded;
}
} }
} }
return 0;
string output2 = "NUTRIENTS TO USE\n";
for (int r = 0; r < amountToUse.GetLength(0); r++)
{
for (int c = 0; c < amountToUse.GetLength(1); c++)
{
output2 += amountToUse[r, c] + " ";
}
output2 += "\n";
}
Debug.Log(output2);
return true;
} }
public float GetNutrients(float x, float y, float radius) public float GetAvailNutrients(float x, float y, float radius)
{ {
int locX = Mathf.FloorToInt(x * sizeX); int locX = Mathf.FloorToInt(x * sizeX);
int locY = Mathf.FloorToInt(y * sizeY); int locY = Mathf.FloorToInt(y * sizeY);
int ceilRad = Mathf.CeilToInt(radius); int flooredRadius = Mathf.FloorToInt(radius);
float totalNutrients = 0; float totalNutrients = nutrientMap[locX, locY];
for(int r = -ceilRad; r < ceilRad; r++) // Calculate total amount of nutrients available to the plant, at a 1/ (2 * steps from origin) ratio.
for (int r = -flooredRadius; r <= flooredRadius; r++)
{ {
for (int c = -ceilRad; c < ceilRad; c++) int currentLocX = locX + r;
for (int c = -flooredRadius; c <= flooredRadius; c++)
{ {
if((locX + ceilRad > 0 && locX + ceilRad < sizeX) && (locY + ceilRad > 0 && locY + ceilRad < sizeY)) int currentLocY = locY + c;
if (currentLocX < 0 || currentLocX >= nutrientMap.GetLength(0) || currentLocY < 0 || currentLocY >= nutrientMap.GetLength(1))
{ {
int fraction = Mathf.Abs(r) + Mathf.Abs(c);
if(fraction == 0) }
else
{
if (r == 0 && c == 0)
{ {
totalNutrients += nutrientMap[locX + ceilRad, locY + ceilRad];
} }
else else
{ {
totalNutrients += nutrientMap[locX + ceilRad, locY + ceilRad] / (fraction * 2); totalNutrients += nutrientMap[currentLocX, currentLocY] / ((Mathf.Abs(r) + Mathf.Abs(c)) * 2);
} }
} }
} }
} }
return totalNutrients; return totalNutrients;
} }
@@ -127,12 +170,60 @@ public class NutrientController : MonoBehaviour
nutrientMap[locX, locY] -= used; nutrientMap[locX, locY] -= used;
} }
public void AddNutrients(float x, float y, float added) public void AddNutrients(float x, float y, float added, float radius)
{ {
int locX = Mathf.FloorToInt(x * sizeX); int locX = Mathf.FloorToInt(x * sizeX);
int locY = Mathf.FloorToInt(y * sizeY); int locY = Mathf.FloorToInt(y * sizeY);
nutrientMap[locX, locY] += added; int flooredRadius = Mathf.FloorToInt(radius);
float totalDistro = 0;
for (int r = -flooredRadius; r <= flooredRadius; r++)
{
int currentLocX = locX + r;
for (int c = -flooredRadius; c <= flooredRadius; c++)
{
int currentLocY = locY + c;
if (currentLocX >= 0 && currentLocX < nutrientMap.GetLength(0) && currentLocY >= 0 && currentLocY < nutrientMap.GetLength(1))
{
if (r == 0 && c == 0)
{
totalDistro += 1;
}
else
{
totalDistro += 1.0f / ((Mathf.Abs(r) + Mathf.Abs(c)) * 2);
}
}
}
}
for (int r = -flooredRadius; r <= flooredRadius; r++)
{
int currentLocX = locX + r;
for (int c = -flooredRadius; c <= flooredRadius; c++)
{
int currentLocY = locY + c;
if (currentLocX >= 0 && currentLocX < nutrientMap.GetLength(0) && currentLocY >= 0 && currentLocY < nutrientMap.GetLength(1))
{
if(r == 0 && c == 0)
{
nutrientMap[currentLocX, currentLocY] += (added / totalDistro);
}
else
{
nutrientMap[currentLocX, currentLocY] += (added / totalDistro) / ((Mathf.Abs(r) + Mathf.Abs(c)) * 2);
}
if(nutrientMap[currentLocX, currentLocY] > 1)
{
nutrientMap[currentLocX, currentLocY] = 1;
}
}
}
}
} }
private void Render() private void Render()
+90 -23
View File
@@ -1,18 +1,29 @@
using UnityEditor;
using UnityEngine; using UnityEngine;
using System.Collections.Generic;
public class Plant : MonoBehaviour public class Plant : MonoBehaviour
{ {
private float healthMax; public float healthMax;
private float healthCurrent; public float healthCurrent;
private float nutrientNeed; public float nutrientNeed;
private float growthRate; public float growthRate;
private float nutrientsConsumed; public float nutrientsConsumed;
public float nutrientsRequiredStep;
public float size;
public float reproductionChance;
public float reproductionDist;
public bool isReproducing;
public float childXPos;
public float childYPos;
public bool isAlive; public bool isAlive;
private GameObject visualPrefab; public GameObject visualPrefab;
public GameObject visual; public GameObject visual;
public float availNutrients;
public void SetVisual(GameObject visualPrefab) public void SetVisual(GameObject visualPrefab)
{ {
this.visualPrefab = visualPrefab; this.visualPrefab = visualPrefab;
@@ -23,6 +34,8 @@ public class Plant : MonoBehaviour
this.healthMax = healthMax; this.healthMax = healthMax;
this.nutrientNeed = nutrientNeed; this.nutrientNeed = nutrientNeed;
this.growthRate = growthRate; this.growthRate = growthRate;
reproductionChance = 1;
reproductionDist = Random.Range(1f, 500f);
} }
// Start is called once before the first execution of Update after the MonoBehaviour is created // Start is called once before the first execution of Update after the MonoBehaviour is created
@@ -32,41 +45,95 @@ public class Plant : MonoBehaviour
healthCurrent = healthMax; healthCurrent = healthMax;
nutrientsConsumed = 0; nutrientsConsumed = 0;
isAlive = true; isAlive = true;
isReproducing = false;
size = 1f;
} }
// Update is called once per frame // Update is called once per frame
void Update() void Update()
{
}
public void Step(float dTime)
{ {
Terrain terrain = Core.instance.ground; Terrain terrain = Core.instance.ground;
NutrientController nutrientCont = Core.instance.nutrientCont; NutrientController nutrientCont = Core.instance.nutrientCont;
float xBound = terrain.terrainData.size.x;
float yBound = terrain.terrainData.size.z;
float x = transform.position.x / xBound;
float y = transform.position.z / yBound;
float x = transform.position.x / terrain.terrainData.size.x; //availNutrients = nutrientCont.GetAvailNutrients(x, y, size);
float y = transform.position.z / terrain.terrainData.size.z;
float size = visual.transform.localScale.x;
//float nutrientsAvailable = nutrientCont.GetNutrients(x, y, size); nutrientsRequiredStep = Mathf.Pow(size, 2f) * nutrientNeed * dTime;
//float nutrientsNeededFrame = (nutrientNeed * Time.deltaTime); float nutrientDeficit = nutrientCont.PlantNutrientUse(x, y, size, nutrientsRequiredStep);
float nutrientsNeededFrame = (nutrientNeed); if (nutrientDeficit > 0)
bool CanStayAlive = nutrientCont.PlantNutrientUse(x, y, size, nutrientsNeededFrame);
/*if (nutrientsNeededFrame > nutrientsAvailable)
{ {
healthCurrent -= Time.deltaTime * 0.1f * healthMax; healthCurrent -= nutrientDeficit;
nutrientsConsumed += (nutrientsRequiredStep - nutrientDeficit);
} }
else else
{ {
visual.transform.localScale += visual.transform.localScale * (growthRate * Time.deltaTime); float adjustedGrowth = (growthRate * 1.5f) / size;
nutrientCont.UseNutrients(x, y, size, nutrientsNeededFrame); size += adjustedGrowth * dTime;
nutrientsConsumed += nutrientsNeededFrame; if(visual != null)
{
visual.transform.localScale = new Vector3(size, size, size);
}
nutrientsConsumed += nutrientsRequiredStep;
} }
if (healthCurrent <= 0) if (healthCurrent <= 0)
{ {
nutrientCont.AddNutrients(x, y, (nutrientsConsumed * 0.9f)); nutrientCont.AddNutrients(x, y, (nutrientsConsumed * 0.9f), size);
isAlive = false; isAlive = false;
}*/ }
float reproRoll = Random.Range(0.0f, 1f);
if(reproRoll > 1 - (reproductionChance * dTime))
{
float direction;
float xOffset;
float yOffset;
int counter = 0;
do
{
direction = Random.Range(0f, 360f);
xOffset = (reproductionDist + Random.Range(-0.1f * reproductionDist, 0.1f * reproductionDist)) * Mathf.Cos(direction * Mathf.Deg2Rad);
yOffset = (reproductionDist + Random.Range(-0.1f * reproductionDist, 0.1f * reproductionDist)) * Mathf.Sin(direction * Mathf.Deg2Rad);
childXPos = transform.position.x + xOffset;
childYPos = transform.position.y + yOffset;
counter++;
if(counter >= 50)
{
return;
}
} while (childXPos > xBound || childXPos < 0 || childYPos > yBound || childYPos < 0);
isReproducing = true;
}
}
public void childMade()
{
childXPos = 0;
childYPos = 0;
isReproducing = false;
}
private void OnDrawGizmos()
{
Vector3 camPos = SceneView.currentDrawingSceneView.camera.transform.position;
if(Vector3.Distance(camPos, this.transform.position) > 15 * visual.transform.localScale.x)
{
return;
}
GUIStyle big = new GUIStyle();
big.fontSize = 50;
big.normal.textColor = Color.purple;
Handles.Label(transform.position, $"Health: {healthCurrent}\nSize: {size}\nnRequired: {nutrientsRequiredStep}", big);
} }
} }
+38 -4
View File
@@ -9,20 +9,21 @@ public class PlantController : MonoBehaviour
List<GameObject> plants; List<GameObject> plants;
float xBound; float xBound;
float zBound; float zBound;
Terrain terrain;
// Start is called once before the first execution of Update after the MonoBehaviour is created // Start is called once before the first execution of Update after the MonoBehaviour is created
void Start() void Start()
{ {
plants = new List<GameObject>(); plants = new List<GameObject>();
Terrain terrain = Core.instance.ground; terrain = Core.instance.ground;
xBound = terrain.GetComponent<Terrain>().terrainData.size.x; xBound = terrain.GetComponent<Terrain>().terrainData.size.x;
zBound = terrain.GetComponent<Terrain>().terrainData.size.z; zBound = terrain.GetComponent<Terrain>().terrainData.size.z;
for (int i = 0; i < 1; i++) for (int i = 0; i < 100; i++)
{ {
GameObject newPlant = Instantiate(plantPrefab); GameObject newPlant = Instantiate(plantPrefab);
newPlant.GetComponent<Plant>().SetVisual(plantModels[Random.Range(0, plantModels.Count - 1)]); newPlant.GetComponent<Plant>().SetVisual(plantModels[Random.Range(0, plantModels.Count - 1)]);
newPlant.GetComponent<Plant>().SetValues(Random.Range(10f, 100f), Random.Range(0.1f, 0.2f), Random.Range(0.1f, 0.25f)); newPlant.GetComponent<Plant>().SetValues(Random.Range(10f, 100f), Random.Range(0.5f, 1f), Random.Range(0.1f, 0.25f));
float x = Random.Range(0, xBound); float x = Random.Range(0, xBound);
float z = Random.Range(0, zBound); float z = Random.Range(0, zBound);
float y = terrain.SampleHeight(new Vector3(x, 0, z)); float y = terrain.SampleHeight(new Vector3(x, 0, z));
@@ -42,13 +43,46 @@ public class PlantController : MonoBehaviour
{ {
GameObject currentPlant = plants[i]; GameObject currentPlant = plants[i];
Plant plantI = currentPlant.GetComponent<Plant>(); Plant plantI = currentPlant.GetComponent<Plant>();
if(!plantI.isAlive) if (!plantI.isAlive)
{ {
currentPlant.SetActive(false); currentPlant.SetActive(false);
plants.Remove(currentPlant); plants.Remove(currentPlant);
Destroy(currentPlant); Destroy(currentPlant);
i--; i--;
} }
else
{
if (plantI.isReproducing)
{
GameObject child = Instantiate(plantPrefab);
Plant childPlant = child.GetComponent<Plant>();
childPlant.SetVisual(plantI.visualPrefab);
childPlant.SetValues(plantI.healthMax + Random.Range(-0.5f, 0.5f), plantI.nutrientNeed + Random.Range(-0.01f, 0.01f), plantI.growthRate + Random.Range(-0.01f, 0.01f));
float x = plantI.childXPos;
float z = plantI.childYPos;
float y = terrain.SampleHeight(new Vector3(plantI.childXPos, 0, plantI.childYPos));
Vector3 childPos = new Vector3(x, y, z);
Vector3 normal = terrain.terrainData.GetInterpolatedNormal(plantI.childXPos / xBound, plantI.childYPos / zBound);
child.transform.LookAt(normal);
child.transform.Rotate(new Vector3(90, 0, 0));
child.transform.position = childPos;
plants.Add(child);
i++;
plantI.childMade();
}
}
}
}
public void Step(float dTime)
{
foreach(GameObject plant in plants)
{
plant.GetComponent<Plant>().Step(dTime);
} }
} }
} }
+2 -1
View File
@@ -183,6 +183,7 @@ MonoBehaviour:
nutrientCont: {fileID: 23266656} nutrientCont: {fileID: 23266656}
plantCont: {fileID: 23266655} plantCont: {fileID: 23266655}
ground: {fileID: 237381106} ground: {fileID: 237381106}
speedOfStep: 3.84
--- !u!4 &23266658 --- !u!4 &23266658
Transform: Transform:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
@@ -192,7 +193,7 @@ Transform:
m_GameObject: {fileID: 23266654} m_GameObject: {fileID: 23266654}
serializedVersion: 2 serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 429.55267, y: 0, z: 320.2245} m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1} m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0 m_ConstrainProportionsScale: 0
m_Children: [] m_Children: []