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());