diff --git a/src/c#/main/entity/TreeModel.cs b/src/c#/main/entity/TreeModel.cs
new file mode 100644
index 00000000..6febe28b
--- /dev/null
+++ b/src/c#/main/entity/TreeModel.cs
@@ -0,0 +1,262 @@
+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.
+ /// 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) {
+ UnityEngine.Debug.LogWarning($"Invalid tree height {height}, clamping to minimum of 1");
+ height = 1;
+ }
+
+ GameObject treeRoot = new GameObject(name);
+ treeRoot.transform.position = position;
+
+ // 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 = new Vector3(0, height / 2.0f, 0);
+
+ // Create leaves/canopy
+ GameObject leaves = CreateLeaves(height);
+ leaves.transform.parent = treeRoot.transform;
+ leaves.transform.localPosition = new Vector3(0, height - 1, 0);
+
+ 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);
+ }
+
+ ///
+ /// 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");
+
+ 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 (matching original color)
+ Material trunkMaterial = new Material(GetDefaultShader());
+ trunkMaterial.color = new Color(0.5f, 0.25f, 0);
+ 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 (matching original color)
+ Material leavesMaterial = new Material(GetDefaultShader());
+ leavesMaterial.color = Color.green;
+ 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 - 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, -height / 2.0f, z);
+ normals[i] = new Vector3(x, 0, z).normalized;
+ uvs[i] = new Vector2((float)i / segments, 0);
+ }
+
+ // 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 for realism
+ float z = Mathf.Sin(angle) * radius * 0.8f;
+ 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, -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);
+ 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..c3d627a5 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,8 @@ public AppleTree(Vector3 position, int height) : base(EntityType.TREE, "Tree") {
}
public override void createGameObject(Vector3 position) {
- GameObject gameObject = new GameObject();
- gameObject.transform.position = position;
- 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());
+ // Use the new 3D tree model instead of primitives
+ GameObject gameObject = TreeModel.CreateTree(position, height, "AppleTree");
setGameObject(gameObject);
@@ -42,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 c414d187..14dd7aeb 100644
--- a/src/c#/tests/entity/TestAppleTree.cs
+++ b/src/c#/tests/entity/TestAppleTree.cs
@@ -15,18 +15,38 @@ 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.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)
+ 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());