From d1838234a2c27e84b1687f1ea95b333bec8b62fe Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Feb 2026 08:30:02 +0000 Subject: [PATCH 1/7] Initial plan From bfd839d372451fe95479ed090a049048e5649a49 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Feb 2026 08:34:22 +0000 Subject: [PATCH 2/7] Replace tree primitives with procedural 3D model Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com> --- src/c#/main/entity/TreeModel.cs | 210 +++++++++++++++++++++++ src/c#/main/entity/TreeModel.cs.meta | 11 ++ src/c#/main/entity/entities/AppleTree.cs | 22 +-- src/c#/tests/entity/TestAppleTree.cs | 15 +- 4 files changed, 231 insertions(+), 27 deletions(-) create mode 100644 src/c#/main/entity/TreeModel.cs create mode 100644 src/c#/main/entity/TreeModel.cs.meta diff --git a/src/c#/main/entity/TreeModel.cs b/src/c#/main/entity/TreeModel.cs new file mode 100644 index 00000000..230a7b53 --- /dev/null +++ b/src/c#/main/entity/TreeModel.cs @@ -0,0 +1,210 @@ +using UnityEngine; + +namespace beyondnations { + + /// + /// Creates a procedural 3D tree model. + /// This replaces the simple primitive shapes (Cylinder + Cube) with a proper mesh-based tree. + /// Can be easily replaced with an imported 3D model file when available. + /// + public class TreeModel { + + public static GameObject CreateTree(Vector3 position, int height) { + GameObject treeRoot = new GameObject("Tree"); + treeRoot.transform.position = position; + + // Create trunk + GameObject trunk = CreateTrunk(height); + trunk.transform.parent = treeRoot.transform; + trunk.transform.localPosition = Vector3.zero; + + // Create leaves/canopy + GameObject leaves = CreateLeaves(height); + leaves.transform.parent = treeRoot.transform; + leaves.transform.localPosition = new Vector3(0, height - 1, 0); + + return treeRoot; + } + + private static GameObject CreateTrunk(int height) { + GameObject trunk = new GameObject("Trunk"); + + MeshFilter meshFilter = trunk.AddComponent(); + MeshRenderer meshRenderer = trunk.AddComponent(); + + // Create a cylindrical trunk mesh + Mesh trunkMesh = CreateCylinderMesh(0.5f, height, 8); + meshFilter.mesh = trunkMesh; + + // Set brown bark material + Material trunkMaterial = new Material(Shader.Find("Standard")); + trunkMaterial.color = new Color(0.4f, 0.2f, 0.1f); + meshRenderer.material = trunkMaterial; + + return trunk; + } + + private static GameObject CreateLeaves(int height) { + GameObject leaves = new GameObject("Leaves"); + + MeshFilter meshFilter = leaves.AddComponent(); + MeshRenderer meshRenderer = leaves.AddComponent(); + + // Create a spherical canopy mesh + Mesh leavesMesh = CreateSphereMesh(2.5f, 10, 10); + meshFilter.mesh = leavesMesh; + + // Set green foliage material + Material leavesMaterial = new Material(Shader.Find("Standard")); + leavesMaterial.color = new Color(0.1f, 0.6f, 0.1f); + meshRenderer.material = leavesMaterial; + + return leaves; + } + + /// + /// Creates a cylindrical mesh for the trunk + /// + private static Mesh CreateCylinderMesh(float radius, float height, int segments) { + Mesh mesh = new Mesh(); + mesh.name = "CylinderMesh"; + + int vertexCount = segments * 2 + 2; // Top and bottom circles plus centers + Vector3[] vertices = new Vector3[vertexCount]; + Vector3[] normals = new Vector3[vertexCount]; + Vector2[] uvs = new Vector2[vertexCount]; + + // Create vertices + float angleStep = 360f / segments * Mathf.Deg2Rad; + + // Bottom circle + for (int i = 0; i < segments; i++) { + float angle = i * angleStep; + float x = Mathf.Cos(angle) * radius; + float z = Mathf.Sin(angle) * radius; + vertices[i] = new Vector3(x, 0, z); + normals[i] = new Vector3(x, 0, z).normalized; + uvs[i] = new Vector2((float)i / segments, 0); + } + + // Top circle + for (int i = 0; i < segments; i++) { + float angle = i * angleStep; + float x = Mathf.Cos(angle) * radius * 0.8f; // Slightly tapered + float z = Mathf.Sin(angle) * radius * 0.8f; + vertices[segments + i] = new Vector3(x, height, z); + normals[segments + i] = new Vector3(x, 0, z).normalized; + uvs[segments + i] = new Vector2((float)i / segments, 1); + } + + // Center points for caps + vertices[segments * 2] = new Vector3(0, 0, 0); // Bottom center + vertices[segments * 2 + 1] = new Vector3(0, height, 0); // Top center + normals[segments * 2] = Vector3.down; + normals[segments * 2 + 1] = Vector3.up; + uvs[segments * 2] = new Vector2(0.5f, 0.5f); + uvs[segments * 2 + 1] = new Vector2(0.5f, 0.5f); + + // Create triangles + int[] triangles = new int[segments * 12]; + int triIndex = 0; + + // Side triangles + for (int i = 0; i < segments; i++) { + int next = (i + 1) % segments; + + // First triangle + triangles[triIndex++] = i; + triangles[triIndex++] = segments + i; + triangles[triIndex++] = next; + + // Second triangle + triangles[triIndex++] = next; + triangles[triIndex++] = segments + i; + triangles[triIndex++] = segments + next; + } + + // Bottom cap + for (int i = 0; i < segments; i++) { + int next = (i + 1) % segments; + triangles[triIndex++] = segments * 2; + triangles[triIndex++] = next; + triangles[triIndex++] = i; + } + + // Top cap + for (int i = 0; i < segments; i++) { + int next = (i + 1) % segments; + triangles[triIndex++] = segments * 2 + 1; + triangles[triIndex++] = segments + i; + triangles[triIndex++] = segments + next; + } + + mesh.vertices = vertices; + mesh.normals = normals; + mesh.uv = uvs; + mesh.triangles = triangles; + + mesh.RecalculateBounds(); + return mesh; + } + + /// + /// Creates a spherical mesh for the leaves + /// + private static Mesh CreateSphereMesh(float radius, int latitudeSegments, int longitudeSegments) { + Mesh mesh = new Mesh(); + mesh.name = "SphereMesh"; + + int vertexCount = (latitudeSegments + 1) * (longitudeSegments + 1); + Vector3[] vertices = new Vector3[vertexCount]; + Vector3[] normals = new Vector3[vertexCount]; + Vector2[] uvs = new Vector2[vertexCount]; + + int vertIndex = 0; + for (int lat = 0; lat <= latitudeSegments; lat++) { + float theta = lat * Mathf.PI / latitudeSegments; + float sinTheta = Mathf.Sin(theta); + float cosTheta = Mathf.Cos(theta); + + for (int lon = 0; lon <= longitudeSegments; lon++) { + float phi = lon * 2 * Mathf.PI / longitudeSegments; + float sinPhi = Mathf.Sin(phi); + float cosPhi = Mathf.Cos(phi); + + Vector3 normal = new Vector3(cosPhi * sinTheta, cosTheta, sinPhi * sinTheta); + vertices[vertIndex] = normal * radius; + normals[vertIndex] = normal; + uvs[vertIndex] = new Vector2((float)lon / longitudeSegments, (float)lat / latitudeSegments); + vertIndex++; + } + } + + int[] triangles = new int[latitudeSegments * longitudeSegments * 6]; + int triIndex = 0; + + for (int lat = 0; lat < latitudeSegments; lat++) { + for (int lon = 0; lon < longitudeSegments; lon++) { + int current = lat * (longitudeSegments + 1) + lon; + int next = current + longitudeSegments + 1; + + triangles[triIndex++] = current; + triangles[triIndex++] = next; + triangles[triIndex++] = current + 1; + + triangles[triIndex++] = current + 1; + triangles[triIndex++] = next; + triangles[triIndex++] = next + 1; + } + } + + mesh.vertices = vertices; + mesh.normals = normals; + mesh.uv = uvs; + mesh.triangles = triangles; + + mesh.RecalculateBounds(); + return mesh; + } + } +} diff --git a/src/c#/main/entity/TreeModel.cs.meta b/src/c#/main/entity/TreeModel.cs.meta new file mode 100644 index 00000000..93d0a8d4 --- /dev/null +++ b/src/c#/main/entity/TreeModel.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a68d5ee19da7445a9ca1cc8d424b2795 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/src/c#/main/entity/entities/AppleTree.cs b/src/c#/main/entity/entities/AppleTree.cs index 6c505ab7..71fd93a2 100644 --- a/src/c#/main/entity/entities/AppleTree.cs +++ b/src/c#/main/entity/entities/AppleTree.cs @@ -4,8 +4,6 @@ namespace beyondnations { public class AppleTree : Entity { - private GameObject trunk; - private GameObject leaves; private int height; public AppleTree(Vector3 position, int height) : base(EntityType.TREE, "Tree") { @@ -14,25 +12,9 @@ public AppleTree(Vector3 position, int height) : base(EntityType.TREE, "Tree") { } public override void createGameObject(Vector3 position) { - GameObject gameObject = new GameObject(); - gameObject.transform.position = position; + // Use the new 3D tree model instead of primitives + GameObject gameObject = TreeModel.CreateTree(position, height); gameObject.name = "AppleTree"; - - trunk = GameObject.CreatePrimitive(PrimitiveType.Cylinder); - trunk.transform.localScale = new Vector3(1, height, 1); - trunk.GetComponent().material.color = new Color(0.5f, 0.25f, 0); - trunk.transform.position = position; - trunk.transform.parent = gameObject.transform; - trunk.name = "Trunk"; - UnityEngine.Object.Destroy(trunk.GetComponent()); - - leaves = GameObject.CreatePrimitive(PrimitiveType.Cube); - leaves.transform.localScale = new Vector3(3, 3, 3); - leaves.GetComponent().material.color = Color.green; - leaves.transform.position = position + new Vector3(0, height - 1, 0); - leaves.transform.parent = gameObject.transform; - leaves.name = "Leaves"; - UnityEngine.Object.Destroy(leaves.GetComponent()); setGameObject(gameObject); diff --git a/src/c#/tests/entity/TestAppleTree.cs b/src/c#/tests/entity/TestAppleTree.cs index c414d187..7cdd8e28 100644 --- a/src/c#/tests/entity/TestAppleTree.cs +++ b/src/c#/tests/entity/TestAppleTree.cs @@ -15,18 +15,19 @@ public static void testInstantiation() { int height = 5; AppleTree tree = new AppleTree(new Vector3(0, 0, 0), height); - // check + // check - verify tree is created with proper structure UnityEngine.Debug.Assert(tree.getType() == EntityType.TREE); UnityEngine.Debug.Assert(tree.getGameObject().name == "AppleTree"); UnityEngine.Debug.Assert(tree.getGameObject().transform.position == new Vector3(0, 0, 0)); - UnityEngine.Debug.Assert(tree.getGameObject().transform.localScale == new Vector3(1, 1, 1)); - UnityEngine.Debug.Assert(tree.getGameObject().transform.childCount == 2); + UnityEngine.Debug.Assert(tree.getGameObject().transform.childCount == 2); // Trunk and Leaves UnityEngine.Debug.Assert(tree.getGameObject().transform.GetChild(0).name == "Trunk"); - UnityEngine.Debug.Assert(tree.getGameObject().transform.GetChild(0).transform.localScale == new Vector3(1, height, 1)); - UnityEngine.Debug.Assert(tree.getGameObject().transform.GetChild(0).GetComponent().material.color == new Color(0.5f, 0.25f, 0)); UnityEngine.Debug.Assert(tree.getGameObject().transform.GetChild(1).name == "Leaves"); - UnityEngine.Debug.Assert(tree.getGameObject().transform.GetChild(1).transform.localScale == new Vector3(3, 3, 3)); - UnityEngine.Debug.Assert(tree.getGameObject().transform.GetChild(1).GetComponent().material.color == Color.green); + + // Verify trunk and leaves have MeshFilter and MeshRenderer components (3D model) + UnityEngine.Debug.Assert(tree.getGameObject().transform.GetChild(0).GetComponent() != null); + UnityEngine.Debug.Assert(tree.getGameObject().transform.GetChild(0).GetComponent() != null); + UnityEngine.Debug.Assert(tree.getGameObject().transform.GetChild(1).GetComponent() != null); + UnityEngine.Debug.Assert(tree.getGameObject().transform.GetChild(1).GetComponent() != null); // clean up GameObject.Destroy(tree.getGameObject()); From 411b401720790977b7b6be1f06ab68b55e0fb0f5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Feb 2026 08:36:00 +0000 Subject: [PATCH 3/7] Fix tree colors to match original implementation Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com> --- src/c#/main/entity/TreeModel.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/c#/main/entity/TreeModel.cs b/src/c#/main/entity/TreeModel.cs index 230a7b53..0a73a377 100644 --- a/src/c#/main/entity/TreeModel.cs +++ b/src/c#/main/entity/TreeModel.cs @@ -36,9 +36,9 @@ private static GameObject CreateTrunk(int height) { Mesh trunkMesh = CreateCylinderMesh(0.5f, height, 8); meshFilter.mesh = trunkMesh; - // Set brown bark material + // Set brown bark material (matching original color) Material trunkMaterial = new Material(Shader.Find("Standard")); - trunkMaterial.color = new Color(0.4f, 0.2f, 0.1f); + trunkMaterial.color = new Color(0.5f, 0.25f, 0); meshRenderer.material = trunkMaterial; return trunk; @@ -54,9 +54,9 @@ private static GameObject CreateLeaves(int height) { Mesh leavesMesh = CreateSphereMesh(2.5f, 10, 10); meshFilter.mesh = leavesMesh; - // Set green foliage material + // Set green foliage material (matching original color) Material leavesMaterial = new Material(Shader.Find("Standard")); - leavesMaterial.color = new Color(0.1f, 0.6f, 0.1f); + leavesMaterial.color = Color.green; meshRenderer.material = leavesMaterial; return leaves; From 1dcbc619b85eccfb0d88ffa7501c785ab116e31f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Feb 2026 08:36:43 +0000 Subject: [PATCH 4/7] Improve TreeModel API and remove redundant code Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com> --- src/c#/main/entity/TreeModel.cs | 4 ++-- src/c#/main/entity/entities/AppleTree.cs | 3 +-- src/c#/tests/entity/TestAppleTree.cs | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/c#/main/entity/TreeModel.cs b/src/c#/main/entity/TreeModel.cs index 0a73a377..1e988121 100644 --- a/src/c#/main/entity/TreeModel.cs +++ b/src/c#/main/entity/TreeModel.cs @@ -9,8 +9,8 @@ namespace beyondnations { /// public class TreeModel { - public static GameObject CreateTree(Vector3 position, int height) { - GameObject treeRoot = new GameObject("Tree"); + public static GameObject CreateTree(Vector3 position, int height, string name = "Tree") { + GameObject treeRoot = new GameObject(name); treeRoot.transform.position = position; // Create trunk diff --git a/src/c#/main/entity/entities/AppleTree.cs b/src/c#/main/entity/entities/AppleTree.cs index 71fd93a2..f96606a9 100644 --- a/src/c#/main/entity/entities/AppleTree.cs +++ b/src/c#/main/entity/entities/AppleTree.cs @@ -13,8 +13,7 @@ public AppleTree(Vector3 position, int height) : base(EntityType.TREE, "Tree") { public override void createGameObject(Vector3 position) { // Use the new 3D tree model instead of primitives - GameObject gameObject = TreeModel.CreateTree(position, height); - gameObject.name = "AppleTree"; + GameObject gameObject = TreeModel.CreateTree(position, height, "AppleTree"); setGameObject(gameObject); diff --git a/src/c#/tests/entity/TestAppleTree.cs b/src/c#/tests/entity/TestAppleTree.cs index 7cdd8e28..1281cc85 100644 --- a/src/c#/tests/entity/TestAppleTree.cs +++ b/src/c#/tests/entity/TestAppleTree.cs @@ -19,7 +19,7 @@ public static void testInstantiation() { UnityEngine.Debug.Assert(tree.getType() == EntityType.TREE); UnityEngine.Debug.Assert(tree.getGameObject().name == "AppleTree"); UnityEngine.Debug.Assert(tree.getGameObject().transform.position == new Vector3(0, 0, 0)); - UnityEngine.Debug.Assert(tree.getGameObject().transform.childCount == 2); // Trunk and Leaves + UnityEngine.Debug.Assert(tree.getGameObject().transform.childCount == 2); UnityEngine.Debug.Assert(tree.getGameObject().transform.GetChild(0).name == "Trunk"); UnityEngine.Debug.Assert(tree.getGameObject().transform.GetChild(1).name == "Leaves"); From ee07bfb2e4a6c8f3b8766edeaf9df512b47eb162 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Feb 2026 11:50:58 +0000 Subject: [PATCH 5/7] Address code review feedback: Add mesh cleanup, fix positioning, validate height, add shader fallback, improve tests Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com> --- src/c#/main/entity/TreeModel.cs | 65 ++++++++++++++++++++---- src/c#/main/entity/entities/AppleTree.cs | 2 +- src/c#/tests/entity/TestAppleTree.cs | 27 ++++++++-- 3 files changed, 78 insertions(+), 16 deletions(-) diff --git a/src/c#/main/entity/TreeModel.cs b/src/c#/main/entity/TreeModel.cs index 1e988121..f304efb0 100644 --- a/src/c#/main/entity/TreeModel.cs +++ b/src/c#/main/entity/TreeModel.cs @@ -6,17 +6,24 @@ namespace beyondnations { /// Creates a procedural 3D tree model. /// This replaces the simple primitive shapes (Cylinder + Cube) with a proper mesh-based tree. /// Can be easily replaced with an imported 3D model file when available. + /// Note: The trunk has a slight taper (top is 0.8x the bottom radius) for more realistic appearance. /// public class TreeModel { public static GameObject CreateTree(Vector3 position, int height, string name = "Tree") { + // Validate height parameter + if (height <= 0) { + height = 1; + } + GameObject treeRoot = new GameObject(name); treeRoot.transform.position = position; - // Create trunk + // Create trunk - position it to match original primitive cylinder behavior + // Unity's primitive cylinder is centered, so we offset by height/2 to match GameObject trunk = CreateTrunk(height); trunk.transform.parent = treeRoot.transform; - trunk.transform.localPosition = Vector3.zero; + trunk.transform.localPosition = new Vector3(0, height / 2.0f, 0); // Create leaves/canopy GameObject leaves = CreateLeaves(height); @@ -26,6 +33,34 @@ public static GameObject CreateTree(Vector3 position, int height, string name = return treeRoot; } + /// + /// Properly destroys a tree and its associated mesh resources. + /// Call this instead of Object.Destroy to prevent memory leaks. + /// + public static void DestroyTree(GameObject treeRoot) { + if (treeRoot == null) return; + + // Explicitly destroy the meshes to prevent memory leaks + Transform trunk = treeRoot.transform.Find("Trunk"); + if (trunk != null) { + MeshFilter meshFilter = trunk.GetComponent(); + if (meshFilter != null && meshFilter.mesh != null) { + UnityEngine.Object.Destroy(meshFilter.mesh); + } + } + + Transform leaves = treeRoot.transform.Find("Leaves"); + if (leaves != null) { + MeshFilter meshFilter = leaves.GetComponent(); + if (meshFilter != null && meshFilter.mesh != null) { + UnityEngine.Object.Destroy(meshFilter.mesh); + } + } + + // Destroy the tree GameObject + UnityEngine.Object.Destroy(treeRoot); + } + private static GameObject CreateTrunk(int height) { GameObject trunk = new GameObject("Trunk"); @@ -37,7 +72,11 @@ private static GameObject CreateTrunk(int height) { meshFilter.mesh = trunkMesh; // Set brown bark material (matching original color) - Material trunkMaterial = new Material(Shader.Find("Standard")); + Shader shader = Shader.Find("Standard"); + if (shader == null) { + shader = Shader.Find("Unlit/Color"); + } + Material trunkMaterial = new Material(shader); trunkMaterial.color = new Color(0.5f, 0.25f, 0); meshRenderer.material = trunkMaterial; @@ -55,7 +94,11 @@ private static GameObject CreateLeaves(int height) { meshFilter.mesh = leavesMesh; // Set green foliage material (matching original color) - Material leavesMaterial = new Material(Shader.Find("Standard")); + Shader shader = Shader.Find("Standard"); + if (shader == null) { + shader = Shader.Find("Unlit/Color"); + } + Material leavesMaterial = new Material(shader); leavesMaterial.color = Color.green; meshRenderer.material = leavesMaterial; @@ -77,29 +120,29 @@ private static Mesh CreateCylinderMesh(float radius, float height, int segments) // Create vertices float angleStep = 360f / segments * Mathf.Deg2Rad; - // Bottom circle + // Bottom circle - centered at -height/2 to match Unity primitive cylinder for (int i = 0; i < segments; i++) { float angle = i * angleStep; float x = Mathf.Cos(angle) * radius; float z = Mathf.Sin(angle) * radius; - vertices[i] = new Vector3(x, 0, z); + vertices[i] = new Vector3(x, -height / 2.0f, z); normals[i] = new Vector3(x, 0, z).normalized; uvs[i] = new Vector2((float)i / segments, 0); } - // Top circle + // Top circle - at +height/2 to match Unity primitive cylinder for (int i = 0; i < segments; i++) { float angle = i * angleStep; - float x = Mathf.Cos(angle) * radius * 0.8f; // Slightly tapered + float x = Mathf.Cos(angle) * radius * 0.8f; // Slightly tapered for realism float z = Mathf.Sin(angle) * radius * 0.8f; - vertices[segments + i] = new Vector3(x, height, z); + vertices[segments + i] = new Vector3(x, height / 2.0f, z); normals[segments + i] = new Vector3(x, 0, z).normalized; uvs[segments + i] = new Vector2((float)i / segments, 1); } // Center points for caps - vertices[segments * 2] = new Vector3(0, 0, 0); // Bottom center - vertices[segments * 2 + 1] = new Vector3(0, height, 0); // Top center + vertices[segments * 2] = new Vector3(0, -height / 2.0f, 0); // Bottom center + vertices[segments * 2 + 1] = new Vector3(0, height / 2.0f, 0); // Top center normals[segments * 2] = Vector3.down; normals[segments * 2 + 1] = Vector3.up; uvs[segments * 2] = new Vector2(0.5f, 0.5f); diff --git a/src/c#/main/entity/entities/AppleTree.cs b/src/c#/main/entity/entities/AppleTree.cs index f96606a9..c3d627a5 100644 --- a/src/c#/main/entity/entities/AppleTree.cs +++ b/src/c#/main/entity/entities/AppleTree.cs @@ -23,7 +23,7 @@ public override void createGameObject(Vector3 position) { } public override void destroyGameObject() { - UnityEngine.Object.Destroy(getGameObject()); + TreeModel.DestroyTree(getGameObject()); } } } \ No newline at end of file diff --git a/src/c#/tests/entity/TestAppleTree.cs b/src/c#/tests/entity/TestAppleTree.cs index 1281cc85..14dd7aeb 100644 --- a/src/c#/tests/entity/TestAppleTree.cs +++ b/src/c#/tests/entity/TestAppleTree.cs @@ -24,10 +24,29 @@ public static void testInstantiation() { UnityEngine.Debug.Assert(tree.getGameObject().transform.GetChild(1).name == "Leaves"); // Verify trunk and leaves have MeshFilter and MeshRenderer components (3D model) - UnityEngine.Debug.Assert(tree.getGameObject().transform.GetChild(0).GetComponent() != null); - UnityEngine.Debug.Assert(tree.getGameObject().transform.GetChild(0).GetComponent() != null); - UnityEngine.Debug.Assert(tree.getGameObject().transform.GetChild(1).GetComponent() != null); - UnityEngine.Debug.Assert(tree.getGameObject().transform.GetChild(1).GetComponent() != null); + Transform trunkTransform = tree.getGameObject().transform.GetChild(0); + Transform leavesTransform = tree.getGameObject().transform.GetChild(1); + + MeshFilter trunkMeshFilter = trunkTransform.GetComponent(); + MeshRenderer trunkMeshRenderer = trunkTransform.GetComponent(); + MeshFilter leavesMeshFilter = leavesTransform.GetComponent(); + MeshRenderer leavesMeshRenderer = leavesTransform.GetComponent(); + + // Components should exist + UnityEngine.Debug.Assert(trunkMeshFilter != null); + UnityEngine.Debug.Assert(trunkMeshRenderer != null); + UnityEngine.Debug.Assert(leavesMeshFilter != null); + UnityEngine.Debug.Assert(leavesMeshRenderer != null); + + // Meshes should be assigned + UnityEngine.Debug.Assert(trunkMeshFilter.mesh != null); + UnityEngine.Debug.Assert(leavesMeshFilter.mesh != null); + + // Materials should be assigned and have expected colors + UnityEngine.Debug.Assert(trunkMeshRenderer.material != null); + UnityEngine.Debug.Assert(leavesMeshRenderer.material != null); + UnityEngine.Debug.Assert(trunkMeshRenderer.material.color == new Color(0.5f, 0.25f, 0)); + UnityEngine.Debug.Assert(leavesMeshRenderer.material.color == Color.green); // clean up GameObject.Destroy(tree.getGameObject()); From 34672b285b86f691f1bed51dc234aefe2b265c4e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Feb 2026 11:51:48 +0000 Subject: [PATCH 6/7] Extract shader fallback logic to helper method and add final fallback Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com> --- src/c#/main/entity/TreeModel.cs | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/src/c#/main/entity/TreeModel.cs b/src/c#/main/entity/TreeModel.cs index f304efb0..fe8bcc4b 100644 --- a/src/c#/main/entity/TreeModel.cs +++ b/src/c#/main/entity/TreeModel.cs @@ -61,6 +61,22 @@ public static void DestroyTree(GameObject treeRoot) { UnityEngine.Object.Destroy(treeRoot); } + /// + /// Gets a default shader with fallback options. + /// Tries Standard shader first, then Unlit/Color, and finally uses a built-in fallback. + /// + private static Shader GetDefaultShader() { + Shader shader = Shader.Find("Standard"); + if (shader == null) { + shader = Shader.Find("Unlit/Color"); + } + if (shader == null) { + // Final fallback - use the built-in default diffuse shader + shader = Shader.Find("Diffuse"); + } + return shader; + } + private static GameObject CreateTrunk(int height) { GameObject trunk = new GameObject("Trunk"); @@ -72,11 +88,7 @@ private static GameObject CreateTrunk(int height) { meshFilter.mesh = trunkMesh; // Set brown bark material (matching original color) - Shader shader = Shader.Find("Standard"); - if (shader == null) { - shader = Shader.Find("Unlit/Color"); - } - Material trunkMaterial = new Material(shader); + Material trunkMaterial = new Material(GetDefaultShader()); trunkMaterial.color = new Color(0.5f, 0.25f, 0); meshRenderer.material = trunkMaterial; @@ -94,11 +106,7 @@ private static GameObject CreateLeaves(int height) { meshFilter.mesh = leavesMesh; // Set green foliage material (matching original color) - Shader shader = Shader.Find("Standard"); - if (shader == null) { - shader = Shader.Find("Unlit/Color"); - } - Material leavesMaterial = new Material(shader); + Material leavesMaterial = new Material(GetDefaultShader()); leavesMaterial.color = Color.green; meshRenderer.material = leavesMaterial; From 40efa9dee2bfd72cadd93b68b58ca83c1079e4dd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Feb 2026 11:53:46 +0000 Subject: [PATCH 7/7] Add debug warning when invalid tree height is clamped Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com> --- src/c#/main/entity/TreeModel.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/c#/main/entity/TreeModel.cs b/src/c#/main/entity/TreeModel.cs index fe8bcc4b..6febe28b 100644 --- a/src/c#/main/entity/TreeModel.cs +++ b/src/c#/main/entity/TreeModel.cs @@ -13,6 +13,7 @@ public class TreeModel { public static GameObject CreateTree(Vector3 position, int height, string name = "Tree") { // Validate height parameter if (height <= 0) { + UnityEngine.Debug.LogWarning($"Invalid tree height {height}, clamping to minimum of 1"); height = 1; }