瀏覽代碼

Initial Enemies displaying with updated docs about gotchas.

CartBlanche 2 周之前
父節點
當前提交
123cee6ed4
共有 33 個文件被更改,包括 726 次插入35 次删除
  1. 36 0
      Shooter/.config/dotnet-tools.json
  2. 274 0
      Shooter/Core/Components/ModelMeshRenderer.cs
  3. 二進制
      Shooter/Core/Content/Models/HoverBot.fbx
  4. 二進制
      Shooter/Core/Content/Models/[email protected]
  5. 二進制
      Shooter/Core/Content/Models/HoverBot_Fixed.fbm/HoverBot_Body_Albedo.png
  6. 二進制
      Shooter/Core/Content/Models/HoverBot_Fixed.fbx
  7. 二進制
      Shooter/Core/Content/Models/Mesh_Weapon_Primary.fbx
  8. 二進制
      Shooter/Core/Content/Models/Mesh_Weapon_Secondary.fbx
  9. 二進制
      Shooter/Core/Content/Textures/Enemies/HoverBot_Body_AO.png
  10. 二進制
      Shooter/Core/Content/Textures/Enemies/HoverBot_Body_Albedo.png
  11. 二進制
      Shooter/Core/Content/Textures/Enemies/HoverBot_Body_MetallicSmooth.png
  12. 二進制
      Shooter/Core/Content/Textures/Enemies/HoverBot_Glass_Albedo.png
  13. 二進制
      Shooter/Core/Content/Textures/Enemies/HoverBot_Light__Emission.png
  14. 二進制
      Shooter/Core/Content/Textures/Weapons/Blaster_AO.png
  15. 二進制
      Shooter/Core/Content/Textures/Weapons/Blaster_Albedo.png
  16. 二進制
      Shooter/Core/Content/Textures/Weapons/Blaster_Emissive.png
  17. 二進制
      Shooter/Core/Content/Textures/Weapons/Blaster_MetallicSmooth.png
  18. 二進制
      Shooter/Core/Content/Textures/Weapons/Blaster_Normal.png
  19. 二進制
      Shooter/Core/Content/Textures/Weapons/Launcher_AO.png
  20. 二進制
      Shooter/Core/Content/Textures/Weapons/Launcher_Albedo.png
  21. 二進制
      Shooter/Core/Content/Textures/Weapons/Launcher_Emissive.png
  22. 二進制
      Shooter/Core/Content/Textures/Weapons/Launcher_MetallicSmooth.png
  23. 二進制
      Shooter/Core/Content/Textures/Weapons/Launcher_Normal.png
  24. 35 0
      Shooter/Core/Content/fix_hoverbot_textures.py
  25. 二進制
      Shooter/Core/Content/sourceimages/BasicRobot_Body_AlbedoTransparency.png
  26. 1 1
      Shooter/Core/Shooter.Core.csproj
  27. 155 0
      Shooter/Docs/UnityToMonoGame.md
  28. 1 1
      Shooter/Gameplay/Shooter.Gameplay.csproj
  29. 53 23
      Shooter/Graphics/ForwardGraphicsProvider.cs
  30. 1 1
      Shooter/Graphics/Shooter.Graphics.csproj
  31. 1 1
      Shooter/Physics/Shooter.Physics.csproj
  32. 168 7
      Shooter/Platforms/Desktop/Game.cs
  33. 1 1
      Shooter/UI/Shooter.UI.csproj

+ 36 - 0
Shooter/.config/dotnet-tools.json

@@ -0,0 +1,36 @@
+{
+  "version": 1,
+  "isRoot": true,
+  "tools": {
+    "dotnet-mgcb": {
+      "version": "3.8.4",
+      "commands": [
+        "mgcb"
+      ]
+    },
+    "dotnet-mgcb-editor": {
+      "version": "3.8.4",
+      "commands": [
+        "mgcb-editor"
+      ]
+    },
+    "dotnet-mgcb-editor-linux": {
+      "version": "3.8.4",
+      "commands": [
+        "mgcb-editor-linux"
+      ]
+    },
+    "dotnet-mgcb-editor-windows": {
+      "version": "3.8.4",
+      "commands": [
+        "mgcb-editor-windows"
+      ]
+    },
+    "dotnet-mgcb-editor-mac": {
+      "version": "3.8.4",
+      "commands": [
+        "mgcb-editor-mac"
+      ]
+    }
+  }
+}

+ 274 - 0
Shooter/Core/Components/ModelMeshRenderer.cs

@@ -0,0 +1,274 @@
+using Microsoft.Xna.Framework.Graphics;
+using System.Numerics;
+using Shooter.Core.Plugins.Graphics;
+
+namespace Shooter.Core.Components;
+
+/// <summary>
+/// Component for rendering MonoGame Model files (FBX imported through Content Pipeline).
+///
+/// UNITY COMPARISON:
+/// Similar to Unity's combination of:
+/// - MeshFilter (holds mesh data)
+/// - MeshRenderer (renders the mesh)
+/// - Model Importer (FBX import pipeline)
+///
+/// Unity automatically handles FBX imports and creates GameObjects with MeshRenderer.
+/// In MonoGame, we:
+/// 1. Import FBX through Content.mgcb
+/// 2. Load Model with Content.Load&lt;Model&gt;()
+/// 3. Manually render each mesh in the model
+///
+/// EDUCATIONAL NOTE - WHY SEPARATE FROM MeshRenderer?
+///
+/// MeshRenderer uses our custom Mesh class (for procedural geometry like cubes/spheres).
+/// ModelMeshRenderer uses MonoGame's Model class (for imported FBX files).
+///
+/// These are different systems:
+/// - Custom Mesh: We manually create vertices and indices
+/// - Model: MonoGame's Content Pipeline processes FBX and creates optimized data
+///
+/// Unity hides this distinction. MonoGame exposes it for more control.
+/// </summary>
+public class ModelMeshRenderer : EntityComponent
+{
+    private Model? _model;
+    private Vector4 _tintColor = Vector4.One; // White = no tint
+    private bool _visible = true;
+    private float _scale = 1.0f;
+
+    /// <summary>
+    /// The MonoGame Model to render.
+    /// Load this from the Content Pipeline, e.g.:
+    /// renderer.Model = Content.Load&lt;Model&gt;("Models/Mesh_Weapon_Primary");
+    /// </summary>
+    public Model? Model
+    {
+        get => _model;
+        set => _model = value;
+    }
+
+    /// <summary>
+    /// Tint color applied to all meshes in the model.
+    /// Use Vector4(1,1,1,1) for white (no tint).
+    ///
+    /// EDUCATIONAL NOTE - TINTING:
+    /// Tinting multiplies the model's texture colors by this color.
+    /// - Vector4(1, 0, 0, 1) = red tint
+    /// - Vector4(0.5, 0.5, 0.5, 1) = darken 50%
+    /// - Vector4(2, 2, 2, 1) = brighten 2x (emissive effect)
+    ///
+    /// Unity has similar functionality in the Material color property.
+    /// </summary>
+    public Vector4 TintColor
+    {
+        get => _tintColor;
+        set => _tintColor = value;
+    }
+
+    /// <summary>
+    /// Whether this model is currently visible.
+    /// Similar to MeshRenderer.enabled in Unity.
+    /// </summary>
+    public bool Visible
+    {
+        get => _visible;
+        set => _visible = value;
+    }
+
+    /// <summary>
+    /// Uniform scale applied to the model.
+    /// For non-uniform scaling, use the Transform3D component's scale property.
+    ///
+    /// EDUCATIONAL NOTE:
+    /// Sometimes imported models are too large or small.
+    /// Use this for quick adjustments without re-exporting from Unity.
+    ///
+    /// Example:
+    /// - Unity model is 10 units tall
+    /// - MonoGame needs 2 units
+    /// - Set Scale = 0.2f
+    /// </summary>
+    public float Scale
+    {
+        get => _scale;
+        set => _scale = value;
+    }
+
+    public override void Initialize()
+    {
+        base.Initialize();
+        // Model is set by code or loaded from Content Manager
+    }
+
+    /// <summary>
+    /// Render the model using MonoGame's BasicEffect.
+    /// Called by the ForwardGraphicsProvider during the render pass.
+    ///
+    /// EDUCATIONAL NOTE - MODEL RENDERING LOOP:
+    ///
+    /// A Model contains multiple ModelMeshes (for complex objects).
+    /// Each ModelMesh contains multiple ModelMeshParts (for different materials).
+    ///
+    /// Example FBX structure:
+    /// Model "HoverBot"
+    ///   ├── ModelMesh "Body"
+    ///   │     ├── ModelMeshPart (Metal material)
+    ///   │     └── ModelMeshPart (Glass material)
+    ///   └── ModelMesh "Eyes"
+    ///         └── ModelMeshPart (Emissive material)
+    ///
+    /// We iterate through all parts and draw them with the correct transforms.
+    ///
+    /// COMMON UNITY-TO-MONOGAME PORTING ISSUE - TRANSFORM MATRICES:
+    ///
+    /// Unity automatically applies GameObject.transform to all renderers.
+    /// In MonoGame, YOU must build the world matrix from your entity's transform.
+    ///
+    /// If models render at world origin (0,0,0) instead of their entity positions:
+    /// → Check that you're building the SRT matrix (Scale-Rotation-Translation)
+    /// → Matrix multiplication order matters: Scale * Rotation * Translation
+    /// → Remember to actually USE the entity's Position and Rotation!
+    ///
+    /// This is one of the most common issues when porting from Unity.
+    /// Unity hides this complexity; MonoGame exposes it for performance and control.
+    /// </summary>
+    public void Draw(GraphicsDevice graphicsDevice, Matrix4x4 world, Matrix4x4 view, Matrix4x4 projection)
+    {
+        if (!_visible || _model == null)
+            return;
+
+        // Get transform from owner entity
+        var transform = Owner?.GetComponent<Transform3D>();
+        if (transform == null)
+            return;
+
+        // Build world matrix: Scale -> Rotation -> Translation (SRT order)
+        var scaleMatrix = Matrix4x4.CreateScale(_scale);
+        var rotationMatrix = Matrix4x4.CreateFromQuaternion(transform.Rotation);
+        var translationMatrix = Matrix4x4.CreateTranslation(transform.Position);
+
+        // Combine: First scale, then rotate, then translate (right-to-left multiplication)
+        var entityWorld = scaleMatrix * rotationMatrix * translationMatrix;
+        var finalWorld = entityWorld * world;
+
+        // Convert System.Numerics matrices to MonoGame matrices for BasicEffect
+        var mgWorld = ToXnaMatrix(finalWorld);
+        var mgView = ToXnaMatrix(view);
+        var mgProjection = ToXnaMatrix(projection);
+
+        // Draw each mesh in the model
+        foreach (var mesh in _model.Meshes)
+        {
+            // Each mesh has its own transformation within the model
+            var meshWorld = mesh.ParentBone.Transform * mgWorld;
+
+            foreach (var meshPart in mesh.MeshParts)
+            {
+                var effect = meshPart.Effect as BasicEffect;
+                if (effect != null)
+                {
+                    // Set transformation matrices
+                    effect.World = meshWorld;
+                    effect.View = mgView;
+                    effect.Projection = mgProjection;
+
+                    // Apply tint color to diffuse
+                    effect.DiffuseColor = new Microsoft.Xna.Framework.Vector3(
+                        _tintColor.X,
+                        _tintColor.Y,
+                        _tintColor.Z
+                    );
+                    effect.Alpha = _tintColor.W;
+
+                    // Enable default lighting (MonoGame's BasicEffect has simple lighting)
+                    effect.EnableDefaultLighting();
+                    effect.PreferPerPixelLighting = true;
+
+                    // Apply the effect and draw
+                    foreach (var pass in effect.CurrentTechnique.Passes)
+                    {
+                        pass.Apply();
+                        graphicsDevice.SetVertexBuffer(meshPart.VertexBuffer, meshPart.VertexOffset);
+                        graphicsDevice.Indices = meshPart.IndexBuffer;
+                        graphicsDevice.DrawIndexedPrimitives(
+                            PrimitiveType.TriangleList,
+                            0,
+                            meshPart.StartIndex,
+                            meshPart.PrimitiveCount
+                        );
+                    }
+                }
+            }
+        }
+    }
+
+    /// <summary>
+    /// Convert System.Numerics.Matrix4x4 to Microsoft.Xna.Framework.Matrix.
+    ///
+    /// EDUCATIONAL NOTE - WHY TWO MATRIX TYPES?
+    ///
+    /// As explained in UnityToMonoGame.md, we use System.Numerics throughout our codebase
+    /// for SIMD performance and BepuPhysics compatibility.
+    ///
+    /// But MonoGame's BasicEffect requires Microsoft.Xna.Framework.Matrix.
+    /// This conversion only happens at the rendering boundary.
+    ///
+    /// The overhead is minimal compared to the benefits of SIMD math everywhere else.
+    /// </summary>
+    private Microsoft.Xna.Framework.Matrix ToXnaMatrix(Matrix4x4 matrix)
+    {
+        return new Microsoft.Xna.Framework.Matrix(
+            matrix.M11, matrix.M12, matrix.M13, matrix.M14,
+            matrix.M21, matrix.M22, matrix.M23, matrix.M24,
+            matrix.M31, matrix.M32, matrix.M33, matrix.M34,
+            matrix.M41, matrix.M42, matrix.M43, matrix.M44
+        );
+    }
+
+    /// <summary>
+    /// Convenience method to load a model from the Content Manager.
+    ///
+    /// UNITY COMPARISON:
+    /// In Unity, you drag-drop an FBX into a prefab and it's ready.
+    /// In MonoGame, you:
+    /// 1. Add FBX to Content.mgcb
+    /// 2. Build content
+    /// 3. Call this method to load at runtime
+    ///
+    /// Example usage:
+    /// var renderer = entity.AddComponent&lt;ModelMeshRenderer&gt;();
+    /// renderer.LoadModel(contentManager, "Models/Mesh_Weapon_Primary");
+    /// </summary>
+    public void LoadModel(Microsoft.Xna.Framework.Content.ContentManager content, string assetName)
+    {
+        try
+        {
+            _model = content.Load<Model>(assetName);
+            Console.WriteLine($"[ModelMeshRenderer] Loaded model: {assetName}");
+        }
+        catch (Exception ex)
+        {
+            Console.WriteLine($"[ModelMeshRenderer] Failed to load model '{assetName}': {ex.Message}");
+            _model = null;
+        }
+    }
+
+    /// <summary>
+    /// Get the bounding sphere for the entire model.
+    /// Useful for culling and distance calculations.
+    ///
+    /// UNITY COMPARISON:
+    /// Similar to MeshRenderer.bounds in Unity.
+    /// Unity calculates bounds automatically.
+    /// MonoGame stores it in the model data.
+    /// </summary>
+    public Microsoft.Xna.Framework.BoundingSphere? GetBoundingSphere()
+    {
+        if (_model == null || _model.Meshes.Count == 0)
+            return null;
+
+        // Get the first mesh's bounding sphere as approximation
+        return _model.Meshes[0].BoundingSphere;
+    }
+}

二進制
Shooter/Core/Content/Models/HoverBot.fbx


二進制
Shooter/Core/Content/Models/[email protected]


二進制
Shooter/Core/Content/Models/HoverBot_Fixed.fbm/HoverBot_Body_Albedo.png


二進制
Shooter/Core/Content/Models/HoverBot_Fixed.fbx


二進制
Shooter/Core/Content/Models/Mesh_Weapon_Primary.fbx


二進制
Shooter/Core/Content/Models/Mesh_Weapon_Secondary.fbx


二進制
Shooter/Core/Content/Textures/Enemies/HoverBot_Body_AO.png


二進制
Shooter/Core/Content/Textures/Enemies/HoverBot_Body_Albedo.png


二進制
Shooter/Core/Content/Textures/Enemies/HoverBot_Body_MetallicSmooth.png


二進制
Shooter/Core/Content/Textures/Enemies/HoverBot_Glass_Albedo.png


二進制
Shooter/Core/Content/Textures/Enemies/HoverBot_Light__Emission.png


二進制
Shooter/Core/Content/Textures/Weapons/Blaster_AO.png


二進制
Shooter/Core/Content/Textures/Weapons/Blaster_Albedo.png


二進制
Shooter/Core/Content/Textures/Weapons/Blaster_Emissive.png


二進制
Shooter/Core/Content/Textures/Weapons/Blaster_MetallicSmooth.png


二進制
Shooter/Core/Content/Textures/Weapons/Blaster_Normal.png


二進制
Shooter/Core/Content/Textures/Weapons/Launcher_AO.png


二進制
Shooter/Core/Content/Textures/Weapons/Launcher_Albedo.png


二進制
Shooter/Core/Content/Textures/Weapons/Launcher_Emissive.png


二進制
Shooter/Core/Content/Textures/Weapons/Launcher_MetallicSmooth.png


二進制
Shooter/Core/Content/Textures/Weapons/Launcher_Normal.png


+ 35 - 0
Shooter/Core/Content/fix_hoverbot_textures.py

@@ -0,0 +1,35 @@
+import bpy
+import os
+
+# Clear existing scene
+bpy.ops.wm.read_factory_settings(use_empty=True)
+
+# Import the HoverBot FBX
+fbx_input = r"C:\Users\savag\source\repos\CartBlanche\MonoGame\MonoGame-Samples\Shooter\Core\Content\Models\HoverBot.fbx"
+bpy.ops.import_scene.fbx(filepath=fbx_input)
+
+# Get the texture we want to use
+texture_path = r"C:\Users\savag\source\repos\CartBlanche\MonoGame\MonoGame-Samples\Shooter\Core\Content\Textures\Enemies\HoverBot_Body_Albedo.png"
+
+# Fix materials to use the correct texture path
+for mat in bpy.data.materials:
+    if mat.use_nodes:
+        for node in mat.node_tree.nodes:
+            if node.type == 'TEX_IMAGE':
+                # Update the image path
+                if os.path.exists(texture_path):
+                    node.image = bpy.data.images.load(texture_path)
+                    print(f"Updated texture for material {mat.name}")
+
+# Export as FBX with embedded textures
+fbx_output = r"C:\Users\savag\source\repos\CartBlanche\MonoGame\MonoGame-Samples\Shooter\Core\Content\Models\HoverBot_Fixed.fbx"
+bpy.ops.export_scene.fbx(
+    filepath=fbx_output,
+    use_selection=False,
+    path_mode='COPY',  # Copy textures relative to FBX
+    embed_textures=True,  # Embed textures in FBX
+    axis_forward='-Z',
+    axis_up='Y'
+)
+
+print(f"Successfully exported fixed HoverBot to {fbx_output}")

二進制
Shooter/Core/Content/sourceimages/BasicRobot_Body_AlbedoTransparency.png


+ 1 - 1
Shooter/Core/Shooter.Core.csproj

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 <Project Sdk="Microsoft.NET.Sdk">
 
 
   <PropertyGroup>
   <PropertyGroup>
-    <TargetFramework>net8.0</TargetFramework>
+    <TargetFramework>net9.0</TargetFramework>
     <LangVersion>latest</LangVersion>
     <LangVersion>latest</LangVersion>
     <Nullable>enable</Nullable>
     <Nullable>enable</Nullable>
     <ImplicitUsings>enable</ImplicitUsings>
     <ImplicitUsings>enable</ImplicitUsings>

+ 155 - 0
Shooter/Docs/UnityToMonoGame.md

@@ -494,6 +494,161 @@ audio.PlaySound("Shoot", transform.Position);
 
 
 ## Graphics & Rendering
 ## Graphics & Rendering
 
 
+### ⚠️ CRITICAL: Transform Matrices Are Manual
+
+**This is the #1 porting issue from Unity to MonoGame!**
+
+| Unity | MonoGame |
+|-------|----------|
+| **Automatic** - Unity applies `GameObject.transform` to all renderers | **Manual** - YOU must build and apply world matrices |
+
+**The Problem:**
+
+In Unity, when you set `transform.position = new Vector3(10, 5, 2)`, the renderer **automatically** uses that position. In MonoGame, setting the entity's position does **nothing** unless you explicitly build a world matrix and pass it to the renderer.
+
+**Symptom:**
+- ✅ Models load successfully
+- ✅ Models appear in render loop
+- ❌ **All models render at world origin (0,0,0)** regardless of entity position
+
+**Unity (Automatic):**
+```csharp
+// Unity
+GameObject enemy = new GameObject("Enemy");
+enemy.transform.position = new Vector3(10, 5, 2);
+var renderer = enemy.AddComponent<MeshRenderer>();
+// ↑ Renderer AUTOMATICALLY uses transform.position = (10, 5, 2)
+```
+
+**MonoGame (Manual):**
+```csharp
+// MonoGame - WRONG (will render at origin!)
+Entity enemy = new Entity("Enemy");
+enemy.Transform.Position = new Vector3(10, 5, 2);
+var renderer = enemy.AddComponent<ModelMeshRenderer>();
+// ↑ Position is set, but renderer doesn't know about it!
+
+// MonoGame - CORRECT (builds SRT matrix)
+public void Draw(GraphicsDevice device, Matrix4x4 world, Matrix4x4 view, Matrix4x4 projection)
+{
+    // Get entity's transform
+    var transform = Owner.GetComponent<Transform3D>();
+
+    // Build SRT (Scale-Rotation-Translation) matrix
+    var scaleMatrix = Matrix4x4.CreateScale(_scale);
+    var rotationMatrix = Matrix4x4.CreateFromQuaternion(transform.Rotation);
+    var translationMatrix = Matrix4x4.CreateTranslation(transform.Position);
+
+    // Combine matrices (order matters: S * R * T)
+    var entityWorld = scaleMatrix * rotationMatrix * translationMatrix;
+    var finalWorld = entityWorld * world;
+
+    // Now the model renders at the correct position!
+    effect.World = ToXnaMatrix(finalWorld);
+    effect.View = ToXnaMatrix(view);
+    effect.Projection = ToXnaMatrix(projection);
+}
+```
+
+**Matrix Multiplication Order:**
+
+MonoGame uses **row-major** matrices. The transformation order is:
+```
+Final Position = Scale × Rotation × Translation × Vertex
+```
+
+**Common Mistakes:**
+
+❌ **Forgetting to use Position:**
+```csharp
+// WRONG - only applies scale
+var finalWorld = Matrix4x4.CreateScale(_scale) * world;
+```
+
+❌ **Wrong multiplication order:**
+```csharp
+// WRONG - translation happens before rotation!
+var finalWorld = translationMatrix * rotationMatrix * scaleMatrix;
+```
+
+❌ **Not combining with parent transform:**
+```csharp
+// WRONG - ignores parent entity's transform
+var finalWorld = scaleMatrix * rotationMatrix * translationMatrix;
+// Should be: finalWorld = (S * R * T) * parentWorld
+```
+
+✅ **CORRECT:**
+```csharp
+// Build entity's local transform
+var entityLocal = scaleMatrix * rotationMatrix * translationMatrix;
+
+// Combine with parent/world transform
+var finalWorld = entityLocal * world;
+```
+
+**Why Unity Hides This:**
+
+Unity's GameObject system automatically:
+1. Builds the SRT matrix from `transform.position`, `transform.rotation`, `transform.localScale`
+2. Handles parent-child hierarchy matrices
+3. Passes the final world matrix to all renderers
+4. Updates matrices when transform changes
+
+**Why MonoGame Exposes This:**
+
+MonoGame gives you control to:
+1. Optimize matrix calculations (update only when needed)
+2. Use custom transformation logic
+3. Implement instanced rendering efficiently
+4. Understand exactly what's happening
+
+**Real-World Example from This Project:**
+
+Before fix (models at origin):
+```csharp
+public void Draw(GraphicsDevice device, Matrix4x4 world, ...)
+{
+    var transform = Owner.GetComponent<Transform3D>(); // Read transform
+    var scaleMatrix = Matrix4x4.CreateScale(_scale);
+    var finalWorld = scaleMatrix * world; // ← BUG: Didn't use position!
+    // Result: All models render at (0,0,0)
+}
+```
+
+After fix (models at correct positions):
+```csharp
+public void Draw(GraphicsDevice device, Matrix4x4 world, ...)
+{
+    var transform = Owner.GetComponent<Transform3D>();
+
+    // Build complete SRT matrix
+    var scaleMatrix = Matrix4x4.CreateScale(_scale);
+    var rotationMatrix = Matrix4x4.CreateFromQuaternion(transform.Rotation);
+    var translationMatrix = Matrix4x4.CreateTranslation(transform.Position);
+
+    var entityWorld = scaleMatrix * rotationMatrix * translationMatrix;
+    var finalWorld = entityWorld * world;
+    // Result: Models render at their entity positions ✓
+}
+```
+
+**Quick Reference:**
+
+| Transformation | Matrix Function |
+|----------------|-----------------|
+| Scale | `Matrix4x4.CreateScale(scale)` |
+| Rotation (Quaternion) | `Matrix4x4.CreateFromQuaternion(rotation)` |
+| Rotation (Euler) | `Matrix4x4.CreateRotationX/Y/Z(angle)` |
+| Translation | `Matrix4x4.CreateTranslation(position)` |
+| Full SRT | `S * R * T` (in that order!) |
+
+**See Also:**
+- [Core/Components/ModelMeshRenderer.cs](../Core/Components/ModelMeshRenderer.cs) - Full implementation with educational comments
+- [Graphics/ForwardGraphicsProvider.cs](../Graphics/ForwardGraphicsProvider.cs) - Rendering pipeline
+
+---
+
 ### Materials & Rendering
 ### Materials & Rendering
 
 
 | Unity | MonoGame (This Project) |
 | Unity | MonoGame (This Project) |

+ 1 - 1
Shooter/Gameplay/Shooter.Gameplay.csproj

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 <Project Sdk="Microsoft.NET.Sdk">
 
 
   <PropertyGroup>
   <PropertyGroup>
-    <TargetFramework>net8.0</TargetFramework>
+    <TargetFramework>net9.0</TargetFramework>
     <LangVersion>latest</LangVersion>
     <LangVersion>latest</LangVersion>
     <Nullable>enable</Nullable>
     <Nullable>enable</Nullable>
     <ImplicitUsings>enable</ImplicitUsings>
     <ImplicitUsings>enable</ImplicitUsings>

+ 53 - 23
Shooter/Graphics/ForwardGraphicsProvider.cs

@@ -160,66 +160,96 @@ public class ForwardGraphicsProvider : IGraphicsProvider
     
     
     /// <summary>
     /// <summary>
     /// Render all objects in the scene.
     /// Render all objects in the scene.
-    /// 
+    ///
     /// EDUCATIONAL NOTE - RENDERING LOOP:
     /// EDUCATIONAL NOTE - RENDERING LOOP:
-    /// 
+    ///
     /// For each object we want to draw:
     /// For each object we want to draw:
     /// 1. Calculate world matrix (position/rotation/scale)
     /// 1. Calculate world matrix (position/rotation/scale)
     /// 2. Set material properties (color, lighting)
     /// 2. Set material properties (color, lighting)
     /// 3. Set transformation matrices on shader
     /// 3. Set transformation matrices on shader
     /// 4. Draw the mesh (send vertices to GPU)
     /// 4. Draw the mesh (send vertices to GPU)
-    /// 
+    ///
     /// Unity hides this loop inside Camera.Render() and handles:
     /// Unity hides this loop inside Camera.Render() and handles:
     /// - Frustum culling (only draw visible objects)
     /// - Frustum culling (only draw visible objects)
     /// - Sorting (transparent objects last)
     /// - Sorting (transparent objects last)
     /// - Batching (combine similar objects)
     /// - Batching (combine similar objects)
-    /// 
+    ///
     /// We're doing a simple version for education.
     /// We're doing a simple version for education.
     /// Phase 2 will add culling and sorting.
     /// Phase 2 will add culling and sorting.
+    ///
+    /// PHASE 2 UPDATE - MODEL RENDERING:
+    /// Now supports both:
+    /// - IRenderable (custom procedural meshes - cubes, spheres)
+    /// - ModelMeshRenderer (FBX models from Content Pipeline)
     /// </summary>
     /// </summary>
     public void RenderScene(ICamera camera, IEnumerable<IRenderable> renderables)
     public void RenderScene(ICamera camera, IEnumerable<IRenderable> renderables)
     {
     {
         if (_graphicsDevice == null || _basicEffect == null)
         if (_graphicsDevice == null || _basicEffect == null)
             return;
             return;
-            
+
         _currentCamera = camera;
         _currentCamera = camera;
-        
+
         // Set up camera matrices on the effect
         // Set up camera matrices on the effect
         // These transform vertices from world space to screen space
         // These transform vertices from world space to screen space
         _basicEffect.View = ToXnaMatrix(camera.ViewMatrix);
         _basicEffect.View = ToXnaMatrix(camera.ViewMatrix);
         _basicEffect.Projection = ToXnaMatrix(camera.ProjectionMatrix);
         _basicEffect.Projection = ToXnaMatrix(camera.ProjectionMatrix);
-        
+
         // Set up lighting
         // Set up lighting
         ConfigureLighting(_basicEffect, _lighting);
         ConfigureLighting(_basicEffect, _lighting);
-        
-        // Draw each renderable object
+
+        // Draw each renderable object (custom meshes)
         foreach (var renderable in renderables)
         foreach (var renderable in renderables)
         {
         {
             if (!renderable.Visible)
             if (!renderable.Visible)
                 continue;
                 continue;
-                
+
             DrawRenderable(renderable);
             DrawRenderable(renderable);
         }
         }
+    }
 
 
-        // Draw HUD overlay (ammo/reload status)
-        if (_hudFont != null && _playerWeaponController != null)
+    /// <summary>
+    /// Render all ModelMeshRenderer components in the scene.
+    /// This is called after RenderScene() to draw FBX models.
+    ///
+    /// EDUCATIONAL NOTE - WHY SEPARATE RENDERING?
+    /// We separate procedural meshes (cubes/spheres) from imported models because:
+    /// - Different data structures (Mesh vs Model)
+    /// - Different effects (our BasicEffect vs Model's embedded effects)
+    /// - Different optimization strategies
+    ///
+    /// Unity combines these automatically.
+    /// MonoGame exposes the distinction for more control.
+    /// </summary>
+    public void RenderModels(ICamera camera, IEnumerable<Core.Components.ModelMeshRenderer> modelRenderers)
+    {
+        if (_graphicsDevice == null)
+        {
+            Console.WriteLine("[RenderModels] ERROR: GraphicsDevice is null!");
+            return;
+        }
+
+        // Draw each model
+        foreach (var modelRenderer in modelRenderers)
         {
         {
-            var weapon = _playerWeaponController.CurrentWeapon;
-            if (weapon != null && _graphicsDevice != null)
+            if (!modelRenderer.Visible)
             {
             {
-                string hudText = $"{weapon.Name} | Ammo: {weapon.CurrentAmmoInMag}/{weapon.CurrentReserveAmmo}";
-                if (weapon.IsReloading)
-                    hudText += " | Reloading...";
-                else if (weapon.CurrentAmmoInMag == 0)
-                    hudText += " | OUT OF AMMO!";
+                Console.WriteLine($"[RenderModels] Skipping invisible model");
+                continue;
+            }
 
 
-                SpriteBatch spriteBatch = new SpriteBatch(_graphicsDevice);
-                spriteBatch.Begin();
-                spriteBatch.DrawString(_hudFont, hudText, new Microsoft.Xna.Framework.Vector2(20, 20), Microsoft.Xna.Framework.Color.White);
-                spriteBatch.End();
+            if (modelRenderer.Model == null)
+            {
+                Console.WriteLine($"[RenderModels] Skipping null model");
+                continue;
             }
             }
+
+            Console.WriteLine($"[RenderModels] Drawing model with {modelRenderer.Model.Meshes.Count} meshes, scale={modelRenderer.Scale}");
+
+            // ModelMeshRenderer handles its own drawing (it has MonoGame's BasicEffect embedded)
+            modelRenderer.Draw(_graphicsDevice, Matrix.Identity, camera.ViewMatrix, camera.ProjectionMatrix);
         }
         }
     }
     }
+
     
     
     /// <summary>
     /// <summary>
     /// Draw a single renderable object.
     /// Draw a single renderable object.

+ 1 - 1
Shooter/Graphics/Shooter.Graphics.csproj

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 <Project Sdk="Microsoft.NET.Sdk">
 
 
   <PropertyGroup>
   <PropertyGroup>
-    <TargetFramework>net8.0</TargetFramework>
+    <TargetFramework>net9.0</TargetFramework>
     <LangVersion>latest</LangVersion>
     <LangVersion>latest</LangVersion>
     <Nullable>enable</Nullable>
     <Nullable>enable</Nullable>
     <ImplicitUsings>enable</ImplicitUsings>
     <ImplicitUsings>enable</ImplicitUsings>

+ 1 - 1
Shooter/Physics/Shooter.Physics.csproj

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 <Project Sdk="Microsoft.NET.Sdk">
 
 
   <PropertyGroup>
   <PropertyGroup>
-    <TargetFramework>net8.0</TargetFramework>
+    <TargetFramework>net9.0</TargetFramework>
     <LangVersion>latest</LangVersion>
     <LangVersion>latest</LangVersion>
     <Nullable>enable</Nullable>
     <Nullable>enable</Nullable>
     <ImplicitUsings>enable</ImplicitUsings>
     <ImplicitUsings>enable</ImplicitUsings>

+ 168 - 7
Shooter/Platforms/Desktop/Game.cs

@@ -182,8 +182,9 @@ public class ShooterGame : Game
     CreateCube(scene, "YellowCube", new System.Numerics.Vector3(3, 3, 0), 2.0f, Color.Yellow);
     CreateCube(scene, "YellowCube", new System.Numerics.Vector3(3, 3, 0), 2.0f, Color.Yellow);
         
         
         // Create enemy entities for AI testing (bright red, taller/skinnier to distinguish from cubes)
         // Create enemy entities for AI testing (bright red, taller/skinnier to distinguish from cubes)
-        CreateEnemy(scene, playerEntity, "Enemy1", new System.Numerics.Vector3(-5, 2, -5), 1.0f, 0.0f, 0.0f);
-        CreateEnemy(scene, playerEntity, "Enemy2", new System.Numerics.Vector3(5, 2, -5), 1.0f, 0.0f, 0.0f);
+        // Position them right next to the weapon for visibility testing
+        CreateEnemy(scene, playerEntity, "Enemy1", new System.Numerics.Vector3(-1, 1.5f, 2), 1.0f, 0.0f, 0.0f);
+        CreateEnemy(scene, playerEntity, "Enemy2", new System.Numerics.Vector3(1, 1.5f, 2), 1.0f, 0.0f, 0.0f);
         
         
         // Initialize the scene
         // Initialize the scene
         scene.Initialize();
         scene.Initialize();
@@ -290,13 +291,28 @@ public class ShooterGame : Game
     /// <summary>
     /// <summary>
     /// Load content (textures, models, sounds).
     /// Load content (textures, models, sounds).
     /// Similar to Unity's resource loading but more manual.
     /// Similar to Unity's resource loading but more manual.
+    ///
+    /// EDUCATIONAL NOTE - CONTENT LOADING:
+    /// Unity automatically loads assets when you reference them in prefabs.
+    /// MonoGame requires explicit loading via Content.Load<T>(assetName).
+    ///
+    /// The Content Manager:
+    /// - Loads .xnb files (compiled assets from Content.mgcb)
+    /// - Caches loaded assets (calling Load twice returns the same instance)
+    /// - Handles asset dependencies automatically
+    ///
+    /// Common asset types:
+    /// - Model: 3D models from FBX files
+    /// - Texture2D: Images (PNG, JPG)
+    /// - SpriteFont: Fonts for text rendering
+    /// - SoundEffect: Audio files
     /// </summary>
     /// </summary>
     protected override void LoadContent()
     protected override void LoadContent()
     {
     {
         _spriteBatch = new SpriteBatch(GraphicsDevice);
         _spriteBatch = new SpriteBatch(GraphicsDevice);
 
 
         // Load SpriteFont for HUD
         // Load SpriteFont for HUD
-        SpriteFont hudFont = Content.Load<SpriteFont>("DefaultFont");
+        SpriteFont hudFont = Content.Load<SpriteFont>("font");
 
 
         // Create Phase 2 test scene AFTER all services are initialized
         // Create Phase 2 test scene AFTER all services are initialized
         CreateTestScene();
         CreateTestScene();
@@ -313,7 +329,133 @@ public class ShooterGame : Game
                 graphicsProvider.SetPlayerWeaponController(weaponController);
                 graphicsProvider.SetPlayerWeaponController(weaponController);
                 Console.WriteLine("[Game] HUD overlay wired up");
                 Console.WriteLine("[Game] HUD overlay wired up");
             }
             }
+
+            // Load and attach weapon model to player's view
+            LoadWeaponModel(playerEntity);
+
+            Console.WriteLine("[Game] Weapon model loaded and attached to player camera");
         }
         }
+
+        // Load enemy models for all enemy entities
+        LoadEnemyModels();
+    }
+
+    /// <summary>
+    /// Load and attach a weapon model to the player's camera for first-person view.
+    ///
+    /// UNITY COMPARISON - WEAPON VIEW MODEL:
+    /// In Unity FPS games, weapons are typically:
+    /// 1. Child objects of the camera GameObject
+    /// 2. Use a separate "Weapon" layer rendered by a second camera
+    /// 3. Have custom FOV to prevent distortion
+    ///
+    /// In MonoGame, we:
+    /// 1. Create a child entity of the player
+    /// 2. Position it relative to the camera's view
+    /// 3. Render it in the normal scene (no separate camera needed for this demo)
+    ///
+    /// EDUCATIONAL NOTE - FPS WEAPON POSITIONING:
+    /// The weapon model needs careful positioning:
+    /// - Too close: fills the screen
+    /// - Too far: looks tiny
+    /// - Wrong rotation: points at weird angles
+    ///
+    /// Unity's values (from FPS Microgame):
+    /// - Position: (0.3, -0.2, 0.5) relative to camera
+    /// - Rotation: Slightly tilted for visual interest
+    /// - Scale: Often enlarged because camera is so close
+    /// </summary>
+    private void LoadWeaponModel(Core.Entities.Entity playerEntity)
+    {
+        try
+        {
+            // Create a weapon entity as a "child" of the player
+            var weaponEntity = new Core.Entities.Entity("PlayerWeaponViewModel");
+            var weaponTransform = weaponEntity.AddComponent<Core.Components.Transform3D>();
+
+            // Position weapon in front of camera (right, down, forward relative to view)
+            // These values are tuned to match Unity FPS Microgame's weapon position
+            // Note: This is a static position for now - Phase 3 will attach to camera transform
+            // Camera at (0, 2, 10) looks toward -Z, so weapon at Z=9 is in front
+            weaponTransform.Position = playerEntity.Transform.Position + new System.Numerics.Vector3(0.5f, -0.5f, -1.5f);
+
+            // Add ModelMeshRenderer and load the weapon model
+            var weaponRenderer = weaponEntity.AddComponent<Core.Components.ModelMeshRenderer>();
+            weaponRenderer.LoadModel(Content, "Models/Mesh_Weapon_Primary");
+            weaponRenderer.Scale = 0.3f; // Much smaller - Unity models are often too large
+            weaponRenderer.TintColor = new System.Numerics.Vector4(1, 1, 1, 1); // No tint
+
+            // Add to scene
+            var scene = _sceneManager?.ActiveScene;
+            if (scene != null)
+            {
+                scene.AddEntity(weaponEntity);
+                weaponEntity.Initialize();
+                Console.WriteLine("[LoadContent] Loaded weapon model 'Mesh_Weapon_Primary' for player view");
+            }
+        }
+        catch (Exception ex)
+        {
+            Console.WriteLine($"[LoadContent] Failed to load weapon model: {ex.Message}");
+            Console.WriteLine("  Make sure Content.mgcb has been built and Models/Mesh_Weapon_Primary.fbx is included");
+        }
+    }
+
+    /// <summary>
+    /// Load 3D models for all enemy entities in the scene.
+    ///
+    /// EDUCATIONAL NOTE - REPLACING PLACEHOLDER MESHES:
+    /// During development, we use colored cubes as placeholders.
+    /// Now we replace the MeshRenderer (cubes) with ModelMeshRenderer (FBX models).
+    ///
+    /// This is a common pattern:
+    /// 1. Phase 1: Get gameplay working with primitives (cubes, spheres)
+    /// 2. Phase 2: Replace with actual art assets
+    /// 3. Phase 3: Add animations, effects, polish
+    ///
+    /// Unity hides this process because you usually import FBX files directly.
+    /// In MonoGame, we make it explicit for learning purposes.
+    /// </summary>
+    private void LoadEnemyModels()
+    {
+        var scene = _sceneManager?.ActiveScene;
+        if (scene == null)
+            return;
+
+        // Find all enemy entities (tagged as "Enemy")
+        var enemies = scene.FindEntitiesByTag("Enemy");
+
+        foreach (var enemy in enemies)
+        {
+            try
+            {
+                // Remove the old MeshRenderer (colored cube placeholder)
+                var oldRenderer = enemy.GetComponent<Core.Components.MeshRenderer>();
+                if (oldRenderer != null)
+                {
+                    enemy.RemoveComponent<Core.Components.MeshRenderer>();
+                }
+
+                // Add ModelMeshRenderer with HoverBot model
+                var modelRenderer = enemy.AddComponent<Core.Components.ModelMeshRenderer>();
+                modelRenderer.LoadModel(Content, "Models/HoverBot_Fixed");
+                modelRenderer.Scale = 0.015f; // Scale down from Unity size (Unity units ~= 1m, adjust to fit MonoGame scene)
+                modelRenderer.TintColor = new System.Numerics.Vector4(1, 0, 0, 1); // Red tint to distinguish enemies
+
+                // Re-initialize the component
+                modelRenderer.Initialize();
+
+                var enemyPos = enemy.Transform.Position;
+                Console.WriteLine($"[LoadContent] Loaded HoverBot model for enemy '{enemy.Name}' at position ({enemyPos.X}, {enemyPos.Y}, {enemyPos.Z})");
+            }
+            catch (Exception ex)
+            {
+                Console.WriteLine($"[LoadContent] Failed to load model for enemy '{enemy.Name}': {ex.Message}");
+                Console.WriteLine("  Enemy will remain as colored cube placeholder");
+            }
+        }
+
+        Console.WriteLine($"[LoadContent] Loaded models for {enemies.Count()} enemy entities");
     }
     }
 
 
     /// <summary>
     /// <summary>
@@ -408,22 +550,41 @@ public class ShooterGame : Game
         {
         {
             // Collect all renderable entities from the scene
             // Collect all renderable entities from the scene
             var renderables = new System.Collections.Generic.List<IRenderable>();
             var renderables = new System.Collections.Generic.List<IRenderable>();
-            
+            var modelRenderers = new System.Collections.Generic.List<Core.Components.ModelMeshRenderer>();
+
             if (_sceneManager?.ActiveScene != null)
             if (_sceneManager?.ActiveScene != null)
             {
             {
                 foreach (var entity in _sceneManager.ActiveScene.Entities)
                 foreach (var entity in _sceneManager.ActiveScene.Entities)
                 {
                 {
-                    // MeshRenderer implements IRenderable
+                    // MeshRenderer implements IRenderable (procedural cubes/spheres)
                     var meshRenderer = entity.GetComponent<Core.Components.MeshRenderer>();
                     var meshRenderer = entity.GetComponent<Core.Components.MeshRenderer>();
                     if (meshRenderer != null)
                     if (meshRenderer != null)
                     {
                     {
                         renderables.Add(meshRenderer);
                         renderables.Add(meshRenderer);
                     }
                     }
+
+                    // ModelMeshRenderer for FBX models from Content Pipeline
+                    var modelRenderer = entity.GetComponent<Core.Components.ModelMeshRenderer>();
+                    if (modelRenderer != null)
+                    {
+                        var pos = entity.Transform.Position;
+                        Console.WriteLine($"[Render] Found ModelMeshRenderer on '{entity.Name}' at ({pos.X:F2}, {pos.Y:F2}, {pos.Z:F2})");
+                        modelRenderers.Add(modelRenderer);
+                    }
                 }
                 }
             }
             }
-            
-            // Render the scene
+
+            // Debug: Log what we're about to render
+            Console.WriteLine($"[Render] Found {renderables.Count} procedural meshes, {modelRenderers.Count} models");
+
+            // Render the scene (procedural meshes first)
             graphics.RenderScene(camera, renderables);
             graphics.RenderScene(camera, renderables);
+
+            // Render FBX models
+            if (graphics is ForwardGraphicsProvider forwardGraphics)
+            {
+                forwardGraphics.RenderModels(camera, modelRenderers);
+            }
         }
         }
         else
         else
         {
         {

+ 1 - 1
Shooter/UI/Shooter.UI.csproj

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 <Project Sdk="Microsoft.NET.Sdk">
 
 
   <PropertyGroup>
   <PropertyGroup>
-    <TargetFramework>net8.0</TargetFramework>
+    <TargetFramework>net9.0</TargetFramework>
     <LangVersion>latest</LangVersion>
     <LangVersion>latest</LangVersion>
     <Nullable>enable</Nullable>
     <Nullable>enable</Nullable>
     <ImplicitUsings>enable</ImplicitUsings>
     <ImplicitUsings>enable</ImplicitUsings>