From 6632f02d1cedc3fb3fc672b15af55d0099403fac Mon Sep 17 00:00:00 2001 From: Alex Stewart Date: Sat, 9 Aug 2025 20:31:25 -0700 Subject: [PATCH] Updated plant behavior V1- Don't like how the variables are interacting. Will be rewriting plant behavior. --- .gitignore | 8 + Assets/GameAssets/Scripts/Core.cs | 15 +- .../GameAssets/Scripts/NutrientController.cs | 171 ++++++++++++++---- Assets/GameAssets/Scripts/Plant.cs | 115 +++++++++--- Assets/GameAssets/Scripts/PlantController.cs | 42 ++++- Assets/Scenes/SampleScene.unity | 3 +- 6 files changed, 282 insertions(+), 72 deletions(-) diff --git a/.gitignore b/.gitignore index 9eb70ce..d92b88f 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,9 @@ *.blend1 *.blend1.meta +# Imported assets +.Assets/Samples/ + # MemoryCaptures can get excessive in size. # They also could contain extremely sensitive data /[Mm]emoryCaptures/ @@ -97,3 +100,8 @@ InitTestScene*.unity* # Auto-generated scenes by play mode tests /[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 diff --git a/Assets/GameAssets/Scripts/Core.cs b/Assets/GameAssets/Scripts/Core.cs index e829417..f1dd11a 100644 --- a/Assets/GameAssets/Scripts/Core.cs +++ b/Assets/GameAssets/Scripts/Core.cs @@ -6,6 +6,9 @@ public class Core : MonoBehaviour [SerializeField] public NutrientController nutrientCont; [SerializeField] public PlantController plantCont; [SerializeField] public Terrain ground; + [SerializeField, Range(0, 10)] public float speedOfStep; + + float timeSoFar; public static Core instance; @@ -16,13 +19,19 @@ public class Core : MonoBehaviour // Start is called once before the first execution of Update after the MonoBehaviour is created void Start() - { - + { + timeSoFar = 0; } // Update is called once per frame void Update() { - + timeSoFar += Time.deltaTime; + if(timeSoFar >= speedOfStep) + { + plantCont.Step(timeSoFar); + nutrientCont.Step(timeSoFar); + timeSoFar = 0; + } } } diff --git a/Assets/GameAssets/Scripts/NutrientController.cs b/Assets/GameAssets/Scripts/NutrientController.cs index a7ce03e..86237e1 100644 --- a/Assets/GameAssets/Scripts/NutrientController.cs +++ b/Assets/GameAssets/Scripts/NutrientController.cs @@ -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 locY = Mathf.FloorToInt(y * sizeY); @@ -38,84 +52,113 @@ public class NutrientController : MonoBehaviour int flooredRadius = Mathf.FloorToInt(radius); 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++) { int currentLocX = locX + r; for(int c = -flooredRadius; c <= flooredRadius; c++) { - int currentLocY = locY + c; - - if(r == 0 && c == 0) + int currentLocY = locY + c; + if (currentLocX < 0 || currentLocX >= nutrientMap.GetLength(0) || currentLocY < 0 || currentLocY >= nutrientMap.GetLength(1)) { - nutrientsAvailable[flooredRadius, flooredRadius] = nutrientMap[currentLocX, currentLocY]; + nutrientsAvailable[flooredRadius, flooredRadius] = 0; } 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 (r == 0 && c == 0) + { + 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) { - 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 = 0; r < nutrientsAvailable.GetLength(0); r++) + for (int r = -flooredRadius; r <= flooredRadius; 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; + } } } - - 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; + return 0; } - 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 locY = Mathf.FloorToInt(y * sizeY); - int ceilRad = Mathf.CeilToInt(radius); - float totalNutrients = 0; + int flooredRadius = Mathf.FloorToInt(radius); + 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 { - totalNutrients += nutrientMap[locX + ceilRad, locY + ceilRad] / (fraction * 2); + totalNutrients += nutrientMap[currentLocX, currentLocY] / ((Mathf.Abs(r) + Mathf.Abs(c)) * 2); } } } } - return totalNutrients; } @@ -127,12 +170,60 @@ public class NutrientController : MonoBehaviour 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 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() diff --git a/Assets/GameAssets/Scripts/Plant.cs b/Assets/GameAssets/Scripts/Plant.cs index 264af1d..da03398 100644 --- a/Assets/GameAssets/Scripts/Plant.cs +++ b/Assets/GameAssets/Scripts/Plant.cs @@ -1,18 +1,29 @@ +using UnityEditor; using UnityEngine; -using System.Collections.Generic; public class Plant : MonoBehaviour { - private float healthMax; - private float healthCurrent; - private float nutrientNeed; - private float growthRate; - private float nutrientsConsumed; + public float healthMax; + public float healthCurrent; + public float nutrientNeed; + public float growthRate; + 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; - private GameObject visualPrefab; + public GameObject visualPrefab; public GameObject visual; + public float availNutrients; + public void SetVisual(GameObject visualPrefab) { this.visualPrefab = visualPrefab; @@ -23,6 +34,8 @@ public class Plant : MonoBehaviour this.healthMax = healthMax; this.nutrientNeed = nutrientNeed; 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 @@ -32,41 +45,95 @@ public class Plant : MonoBehaviour healthCurrent = healthMax; nutrientsConsumed = 0; isAlive = true; + isReproducing = false; + size = 1f; } // Update is called once per frame void Update() + { + + } + + public void Step(float dTime) { 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; - float y = transform.position.z / terrain.terrainData.size.z; - float size = visual.transform.localScale.x; + //availNutrients = nutrientCont.GetAvailNutrients(x, y, size); - //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); - - bool CanStayAlive = nutrientCont.PlantNutrientUse(x, y, size, nutrientsNeededFrame); - - /*if (nutrientsNeededFrame > nutrientsAvailable) + if (nutrientDeficit > 0) { - healthCurrent -= Time.deltaTime * 0.1f * healthMax; + healthCurrent -= nutrientDeficit; + nutrientsConsumed += (nutrientsRequiredStep - nutrientDeficit); } else { - visual.transform.localScale += visual.transform.localScale * (growthRate * Time.deltaTime); - nutrientCont.UseNutrients(x, y, size, nutrientsNeededFrame); - nutrientsConsumed += nutrientsNeededFrame; + float adjustedGrowth = (growthRate * 1.5f) / size; + size += adjustedGrowth * dTime; + if(visual != null) + { + visual.transform.localScale = new Vector3(size, size, size); + } + nutrientsConsumed += nutrientsRequiredStep; } if (healthCurrent <= 0) { - nutrientCont.AddNutrients(x, y, (nutrientsConsumed * 0.9f)); + nutrientCont.AddNutrients(x, y, (nutrientsConsumed * 0.9f), size); 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); } } diff --git a/Assets/GameAssets/Scripts/PlantController.cs b/Assets/GameAssets/Scripts/PlantController.cs index d09e7cd..5e8d6fc 100644 --- a/Assets/GameAssets/Scripts/PlantController.cs +++ b/Assets/GameAssets/Scripts/PlantController.cs @@ -9,20 +9,21 @@ public class PlantController : MonoBehaviour List plants; float xBound; float zBound; + Terrain terrain; // Start is called once before the first execution of Update after the MonoBehaviour is created void Start() { plants = new List(); - Terrain terrain = Core.instance.ground; + terrain = Core.instance.ground; xBound = terrain.GetComponent().terrainData.size.x; zBound = terrain.GetComponent().terrainData.size.z; - for (int i = 0; i < 1; i++) + for (int i = 0; i < 100; i++) { GameObject newPlant = Instantiate(plantPrefab); newPlant.GetComponent().SetVisual(plantModels[Random.Range(0, plantModels.Count - 1)]); - newPlant.GetComponent().SetValues(Random.Range(10f, 100f), Random.Range(0.1f, 0.2f), Random.Range(0.1f, 0.25f)); + newPlant.GetComponent().SetValues(Random.Range(10f, 100f), Random.Range(0.5f, 1f), Random.Range(0.1f, 0.25f)); float x = Random.Range(0, xBound); float z = Random.Range(0, zBound); float y = terrain.SampleHeight(new Vector3(x, 0, z)); @@ -42,13 +43,46 @@ public class PlantController : MonoBehaviour { GameObject currentPlant = plants[i]; Plant plantI = currentPlant.GetComponent(); - if(!plantI.isAlive) + if (!plantI.isAlive) { currentPlant.SetActive(false); plants.Remove(currentPlant); Destroy(currentPlant); i--; } + else + { + if (plantI.isReproducing) + { + GameObject child = Instantiate(plantPrefab); + Plant childPlant = child.GetComponent(); + 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().Step(dTime); } } } diff --git a/Assets/Scenes/SampleScene.unity b/Assets/Scenes/SampleScene.unity index acefbe6..f6355af 100644 --- a/Assets/Scenes/SampleScene.unity +++ b/Assets/Scenes/SampleScene.unity @@ -183,6 +183,7 @@ MonoBehaviour: nutrientCont: {fileID: 23266656} plantCont: {fileID: 23266655} ground: {fileID: 237381106} + speedOfStep: 3.84 --- !u!4 &23266658 Transform: m_ObjectHideFlags: 0 @@ -192,7 +193,7 @@ Transform: m_GameObject: {fileID: 23266654} serializedVersion: 2 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_ConstrainProportionsScale: 0 m_Children: []