using Microsoft.Xna.Framework.Input;
using Shooter.Core.Services;
using Shooter.Core.Plugins.Physics;
using Shooter.Core.Plugins.Graphics;
using Shooter.Graphics;
using Shooter.Physics;
using Shooter.Gameplay.Systems;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Shooter.Core.Scenes;
using System;
using System.Linq;
namespace Shooter;
///
/// Main game class for MonoGame FPS.
/// This is the entry point for the game, similar to Unity's main scene setup.
///
/// UNITY COMPARISON:
/// Unity doesn't have a single "Game" class. Instead:
/// - This replaces Unity's Application class
/// - Initialize() = Scene load + Awake()
/// - Update() = Update() across all GameObjects
/// - Draw() = Camera rendering (handled by Unity automatically)
///
public class ShooterGame : Game
{
private GraphicsDeviceManager _graphics;
private SpriteBatch? _spriteBatch;
private SceneManager? _sceneManager;
public ShooterGame()
{
_graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
IsMouseVisible = false;
// Set up window
_graphics.PreferredBackBufferWidth = 800;
_graphics.PreferredBackBufferHeight = 600;
_graphics.IsFullScreen = false;
_graphics.SynchronizeWithVerticalRetrace = true;
Window.Title = "Unity Shooter in MonoGame";
}
///
/// Initialize the game.
/// Similar to Unity's Awake() but for the entire application.
///
protected override void Initialize()
{
// Initialize service locator
ServiceLocator.Initialize();
// Phase 1: Register core services
var inputService = new InputService();
var timeService = new TimeService();
var audioService = new AudioService();
var physicsProvider = new BepuPhysicsProvider();
var graphicsProvider = new ForwardGraphicsProvider();
// Phase 2: Register gameplay systems
var projectileSystem = new Gameplay.Systems.ProjectileSystem(physicsProvider);
// Initialize input service with screen center for mouse locking
var screenCenter = new Microsoft.Xna.Framework.Point(
_graphics.PreferredBackBufferWidth / 2,
_graphics.PreferredBackBufferHeight / 2
);
inputService.Initialize(screenCenter);
// Initialize physics and graphics providers
physicsProvider.Initialize();
ServiceLocator.Register(inputService);
ServiceLocator.Register(timeService);
ServiceLocator.Register(audioService);
ServiceLocator.Register(physicsProvider);
ServiceLocator.Register(graphicsProvider);
ServiceLocator.Register(projectileSystem);
// Initialize graphics provider with our GraphicsDevice
graphicsProvider.SetGraphicsDevice(GraphicsDevice);
// Create scene manager
_sceneManager = new SceneManager();
// TODO: Register custom component types
// _sceneManager.RegisterComponentType("PlayerController");
base.Initialize();
Console.WriteLine("MonoGame FPS initialized!");
Console.WriteLine("Phase 1 services registered:");
Console.WriteLine(" - InputService (keyboard, mouse, gamepad)");
Console.WriteLine(" - TimeService (delta time, time scaling)");
Console.WriteLine(" - AudioService (placeholder for Phase 4)");
Console.WriteLine(" - BepuPhysicsProvider (multi-threaded physics)");
Console.WriteLine(" - ForwardGraphicsProvider (BasicEffect rendering)");
}
///
/// Create a simple test scene for Phase 1 validation.
/// This demonstrates:
/// - Camera setup
/// - Static and dynamic physics bodies
/// - Mesh rendering with different colors
/// - Basic 3D scene composition
///
/// PHASE 2 UPDATE:
/// - Player entity with FirstPersonController for movement
/// - Mouse look and WASD controls enabled
///
private void CreateTestScene()
{
// Create a new scene
var scene = new Core.Scenes.Scene("Phase2TestScene");
_sceneManager!.UnloadScene();
// Create Player Entity with FPS Controller
var playerEntity = new Core.Entities.Entity("Player");
playerEntity.Tag = "Player";
var playerTransform = playerEntity.AddComponent();
playerTransform.Position = new System.Numerics.Vector3(0, 2, 10); // Start above ground, back from origin
// Add Camera to player
var camera = playerEntity.AddComponent();
camera.FieldOfView = 75f;
camera.NearPlane = 0.1f;
camera.FarPlane = 100f;
// Add FirstPersonController for movement (kinematic movement, no physics yet)
var fpsController = playerEntity.AddComponent();
fpsController.MoveSpeed = 50.0f; // Tuned for comfortable movement
fpsController.MouseSensitivity = 50.0f; // Higher sensitivity for normalized delta (pixel delta normalized to -1 to 1 range)
fpsController.JumpForce = 8.0f;
// Add WeaponController for weapon management
var weaponController = playerEntity.AddComponent();
// Add Health to player
var playerHealth = playerEntity.AddComponent();
playerHealth.MaxHealth = 100f;
// Add HUD component to player
var hud = playerEntity.AddComponent();
scene.AddEntity(playerEntity);
// Initialize camera AFTER entity is added and components are initialized
camera.Position = playerTransform.Position;
camera.Target = new System.Numerics.Vector3(0, 2, 0); // Look toward origin at same height
// Equip weapons for testing
var pistol = Gameplay.Weapons.Gun.CreatePistol();
var rifle = Gameplay.Weapons.Gun.CreateAssaultRifle();
weaponController.EquipWeapon(pistol, 0); // Slot 1 (press '1' to select)
weaponController.EquipWeapon(rifle, 1); // Slot 2 (press '2' to select)
Console.WriteLine("[Phase 2 Test Scene] Player created with FirstPersonController");
Console.WriteLine(" Controls: WASD = Move, Mouse = Look, Space = Jump, Shift = Sprint, Escape = Release Mouse");
Console.WriteLine(" Weapons: 1 = Pistol, 2 = Assault Rifle, LMB = Fire, R = Reload");
// Create Ground Plane (large flat box)
var groundEntity = new Core.Entities.Entity("Ground");
var groundTransform = groundEntity.AddComponent();
groundTransform.Position = new System.Numerics.Vector3(0, -1, 0);
groundTransform.LocalScale = new System.Numerics.Vector3(20, 0.5f, 20); // Wide and flat
var groundRenderer = groundEntity.AddComponent();
groundRenderer.SetCube(1.0f, new System.Numerics.Vector4(0.3f, 0.5f, 0.3f, 1.0f)); // Green ground
Console.WriteLine($"[DEBUG] Ground cube material color: {groundRenderer.Material.Color}");
var groundRigid = groundEntity.AddComponent();
groundRigid.BodyType = Core.Plugins.Physics.BodyType.Static;
groundRigid.Shape = new Core.Plugins.Physics.BoxShape(20, 0.5f, 20);
scene.AddEntity(groundEntity);
// Create a few colored cubes (larger for easier targeting)
CreateCube(scene, "RedCube", new System.Numerics.Vector3(-3, 2, 0), 2.0f, Color.Red);
CreateCube(scene, "BlueCube", new System.Numerics.Vector3(0, 4, 0), 2.0f, Color.Blue);
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)
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);
// Initialize the scene
scene.Initialize();
Console.WriteLine($"[DEBUG] Scene has {scene.Entities.Count} entities after initialization");
foreach (var ent in scene.Entities)
{
Console.WriteLine($"[DEBUG] - {ent.Name}: Active={ent.Active}, ComponentCount={ent.GetType().GetProperty("Components")?.GetValue(ent)}");
}
// Make it the active scene (hacky but works for testing)
var activeSceneField = typeof(SceneManager).GetField("_activeScene",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
activeSceneField?.SetValue(_sceneManager, scene);
Console.WriteLine("[Phase 2 Test Scene] Created:");
Console.WriteLine(" - Player with FPS controller, weapons, health (100 HP), and HUD at (0, 2, 10)");
Console.WriteLine(" - Ground plane (20x0.5x20 static box)");
Console.WriteLine(" - 3 colored cubes with Health (50 HP each) for target practice");
Console.WriteLine(" - 2 enemy entities with AI (20 HP, 10 damage melee attacks)");
Console.WriteLine(" - Controls: WASD=Move, Mouse=Look, Space=Jump, Shift=Sprint, Esc=Menu");
Console.WriteLine(" - Weapons: 1=Pistol, 2=Assault Rifle, LMB=Fire, R=Reload");
}
///
/// Helper to create a cube entity.
///
private void CreateCube(Core.Scenes.Scene scene, string name,
System.Numerics.Vector3 position, float size, Color color)
{
var entity = new Core.Entities.Entity(name);
var transform = entity.AddComponent();
transform.Position = position;
transform.LocalScale = new System.Numerics.Vector3(size);
var renderer = entity.AddComponent();
renderer.SetCube(1.0f, new System.Numerics.Vector4(color.R / 255f, color.G / 255f, color.B / 255f, 1.0f));
var rigidbody = entity.AddComponent();
rigidbody.BodyType = Core.Plugins.Physics.BodyType.Static; // Static so cubes don't fall
rigidbody.Shape = new Core.Plugins.Physics.BoxShape(size, size, size);
rigidbody.Mass = 1.0f;
// Add Health component for damage testing
var health = entity.AddComponent();
health.MaxHealth = 50f;
health.DestroyOnDeath = false; // Keep cube visible after death for testing
// Subscribe to death event
health.OnDeath += (damageInfo) =>
{
Console.WriteLine($"[Test Scene] {name} has been destroyed!");
};
scene.AddEntity(entity);
}
///
/// Helper to create an enemy entity with AI.
///
private void CreateEnemy(Core.Scenes.Scene scene, Core.Entities.Entity player, string name,
System.Numerics.Vector3 position, float r, float g, float b)
{
var entity = new Core.Entities.Entity(name);
entity.Tag = "Enemy";
var transform = entity.AddComponent();
transform.Position = position;
transform.LocalScale = new System.Numerics.Vector3(0.8f, 1.8f, 0.8f); // Taller, skinnier - more humanoid
var renderer = entity.AddComponent();
renderer.SetCube(1.0f, new System.Numerics.Vector4(r, g, b, 1.0f));
var rigidbody = entity.AddComponent();
rigidbody.BodyType = Core.Plugins.Physics.BodyType.Static; // Static for now (Phase 3 will add dynamic movement)
rigidbody.Shape = new Core.Plugins.Physics.BoxShape(0.8f, 1.8f, 0.8f);
rigidbody.Mass = 70.0f; // Human-like weight
// Add Health component
var health = entity.AddComponent();
health.MaxHealth = 20f;
health.DestroyOnDeath = false; // Keep visible after death for testing
// Subscribe to death event
health.OnDeath += (damageInfo) =>
{
Console.WriteLine($"[Test Scene] {name} was killed by {damageInfo.Attacker?.Name ?? "unknown"}!");
};
// Add EnemyAI component
var enemyAI = entity.AddComponent();
enemyAI.Target = player; // Set player as target
enemyAI.DetectionRange = 15f;
enemyAI.AttackRange = 2.5f;
enemyAI.MoveSpeed = 2.5f;
enemyAI.AttackDamage = 10f;
enemyAI.AttackCooldown = 2.0f;
scene.AddEntity(entity);
Console.WriteLine($"[Test Scene] Created enemy '{name}' at {position} targeting player");
}
///
/// Load content (textures, models, sounds).
/// Similar to Unity's resource loading but more manual.
///
protected override void LoadContent()
{
_spriteBatch = new SpriteBatch(GraphicsDevice);
// TODO Phase 1: Load content via Content Pipeline
// TODO Phase 5: Load Gum UI screens
Console.WriteLine("Content loaded (placeholder - Phase 1 will add actual content)");
// Create Phase 2 test scene AFTER all services are initialized
CreateTestScene();
// Load HUD content after scene is created
var playerEntity = _sceneManager?.ActiveScene?.FindEntitiesByTag("Player").FirstOrDefault();
if (playerEntity != null)
{
var hud = playerEntity.GetComponent();
hud?.LoadContent(GraphicsDevice, _spriteBatch);
Console.WriteLine("[Game] HUD content loaded");
}
}
///
/// Update game logic.
/// This is called every frame, similar to Update() in Unity.
///
/// EDUCATIONAL NOTE - Fixed Timestep Physics:
/// We run physics on a fixed timestep (60 updates/sec) while
/// rendering runs at variable framerate. This ensures:
/// - Deterministic physics simulation
/// - Consistent behavior across different hardware
/// - Decoupled physics from rendering performance
/// Unity does this automatically. In MonoGame we manage it manually.
///
/// Provides timing information
protected override void Update(GameTime gameTime)
{
// Update core services
var timeService = ServiceLocator.Get();
var inputService = ServiceLocator.Get();
var physicsProvider = ServiceLocator.Get();
timeService.Update(gameTime);
inputService.Update();
// Update active scene
var coreGameTime = new Core.Components.GameTime(
gameTime.TotalGameTime,
gameTime.ElapsedGameTime
);
_sceneManager?.Update(coreGameTime);
// Step physics simulation with fixed timestep
// We use the scaled delta time from TimeService to support slow-motion/time effects
// Guard against zero/negative timestep on first frame
if (timeService.DeltaTime > 0)
{
physicsProvider.Step(timeService.DeltaTime);
}
base.Update(gameTime);
}
///
/// Draw/render the game.
/// MonoGame requires you to explicitly handle rendering.
/// Unity does this automatically based on cameras.
///
/// EDUCATIONAL NOTE - Rendering Pipeline:
/// 1. Clear backbuffer (prevent artifacts from previous frame)
/// 2. BeginFrame - Set up render state
/// 3. RenderScene - Draw all renderable entities
/// 4. EndFrame - Present to screen
/// Unity's rendering is automatic via Camera components.
/// MonoGame gives you full control (and responsibility).
///
/// Provides timing information
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.CornflowerBlue);
// Get graphics provider
var graphics = ServiceLocator.Get();
// Begin frame
graphics.BeginFrame();
// Find active camera in scene (attached to Player entity in Phase 2)
// TODO: Improve camera selection (support multiple cameras, camera priorities)
Core.Components.Camera? camera = null;
// Try to find camera on Player entity first
var playerEntity = _sceneManager?.ActiveScene?.FindEntitiesByTag("Player").FirstOrDefault();
if (playerEntity != null)
{
camera = playerEntity.GetComponent();
}
// Fallback to MainCamera tag if no player camera found
if (camera == null)
{
var cameraEntity = _sceneManager?.ActiveScene?.FindEntitiesByTag("MainCamera").FirstOrDefault();
camera = cameraEntity?.GetComponent();
}
if (camera != null)
{
// Collect all renderable entities from the scene
var renderables = new System.Collections.Generic.List();
if (_sceneManager?.ActiveScene != null)
{
foreach (var entity in _sceneManager.ActiveScene.Entities)
{
// MeshRenderer implements IRenderable
var meshRenderer = entity.GetComponent();
if (meshRenderer != null)
{
renderables.Add(meshRenderer);
}
}
}
// Render the scene
graphics.RenderScene(camera, renderables);
}
else
{
Console.WriteLine("[Render] ERROR: No camera found!");
}
// End frame (presents to screen)
graphics.EndFrame();
// Draw crosshair (simple 2D overlay)
DrawCrosshair();
// Draw scene entities (UI/debug overlay - Phase 5)
var coreGameTime = new Core.Components.GameTime(
gameTime.TotalGameTime,
gameTime.ElapsedGameTime
);
_sceneManager?.Draw(coreGameTime);
base.Draw(gameTime);
}
///
/// Draw a simple crosshair in the center of the screen.
/// This helps with aiming when the mouse cursor is hidden.
///
private void DrawCrosshair()
{
if (_spriteBatch == null) return;
_spriteBatch.Begin();
// Get screen center
int centerX = _graphics.PreferredBackBufferWidth / 2;
int centerY = _graphics.PreferredBackBufferHeight / 2;
// Crosshair size
int crosshairSize = 10;
int thickness = 2;
int gap = 3;
// Create a 1x1 white pixel texture for drawing lines
Texture2D pixel = new Texture2D(GraphicsDevice, 1, 1);
pixel.SetData(new[] { Color.White });
// Draw horizontal line (left and right from center)
_spriteBatch.Draw(pixel, new Rectangle(centerX - crosshairSize - gap, centerY - thickness / 2, crosshairSize, thickness), Color.White);
_spriteBatch.Draw(pixel, new Rectangle(centerX + gap, centerY - thickness / 2, crosshairSize, thickness), Color.White);
// Draw vertical line (top and bottom from center)
_spriteBatch.Draw(pixel, new Rectangle(centerX - thickness / 2, centerY - crosshairSize - gap, thickness, crosshairSize), Color.White);
_spriteBatch.Draw(pixel, new Rectangle(centerX - thickness / 2, centerY + gap, thickness, crosshairSize), Color.White);
_spriteBatch.End();
pixel.Dispose();
}
///
/// Cleanup when game exits.
/// Similar to Unity's OnApplicationQuit() or OnDestroy().
///
protected override void UnloadContent()
{
// Unload scene
_sceneManager?.UnloadScene();
// Shutdown services
ServiceLocator.Get()?.Shutdown();
ServiceLocator.Get()?.Shutdown();
ServiceLocator.Clear();
Console.WriteLine("MonoGame FPS shutdown complete");
base.UnloadContent();
}
}