Game.cs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500
  1. using Microsoft.Xna.Framework.Input;
  2. using Shooter.Core.Services;
  3. using Shooter.Core.Plugins.Physics;
  4. using Shooter.Core.Plugins.Graphics;
  5. using Shooter.Graphics;
  6. using Shooter.Physics;
  7. using Shooter.Gameplay.Systems;
  8. using Microsoft.Xna.Framework;
  9. using Microsoft.Xna.Framework.Graphics;
  10. using Shooter.Core.Scenes;
  11. using System;
  12. using System.Linq;
  13. namespace Shooter;
  14. /// <summary>
  15. /// Main game class for MonoGame FPS.
  16. /// This is the entry point for the game, similar to Unity's main scene setup.
  17. ///
  18. /// UNITY COMPARISON:
  19. /// Unity doesn't have a single "Game" class. Instead:
  20. /// - This replaces Unity's Application class
  21. /// - Initialize() = Scene load + Awake()
  22. /// - Update() = Update() across all GameObjects
  23. /// - Draw() = Camera rendering (handled by Unity automatically)
  24. /// </summary>
  25. public class ShooterGame : Game
  26. {
  27. private GraphicsDeviceManager _graphics;
  28. private SpriteBatch? _spriteBatch;
  29. private SceneManager? _sceneManager;
  30. public ShooterGame()
  31. {
  32. _graphics = new GraphicsDeviceManager(this);
  33. Content.RootDirectory = "Content";
  34. IsMouseVisible = false;
  35. // Set up window
  36. _graphics.PreferredBackBufferWidth = 800;
  37. _graphics.PreferredBackBufferHeight = 600;
  38. _graphics.IsFullScreen = false;
  39. _graphics.SynchronizeWithVerticalRetrace = true;
  40. Window.Title = "Unity Shooter in MonoGame";
  41. }
  42. /// <summary>
  43. /// Initialize the game.
  44. /// Similar to Unity's Awake() but for the entire application.
  45. /// </summary>
  46. protected override void Initialize()
  47. {
  48. // Initialize service locator
  49. ServiceLocator.Initialize();
  50. // Phase 1: Register core services
  51. var inputService = new InputService();
  52. var timeService = new TimeService();
  53. var audioService = new AudioService();
  54. var physicsProvider = new BepuPhysicsProvider();
  55. var graphicsProvider = new ForwardGraphicsProvider();
  56. // Phase 2: Register gameplay systems
  57. var projectileSystem = new Gameplay.Systems.ProjectileSystem(physicsProvider);
  58. // Initialize input service with screen center for mouse locking
  59. var screenCenter = new Microsoft.Xna.Framework.Point(
  60. _graphics.PreferredBackBufferWidth / 2,
  61. _graphics.PreferredBackBufferHeight / 2
  62. );
  63. inputService.Initialize(screenCenter);
  64. // Initialize physics and graphics providers
  65. physicsProvider.Initialize();
  66. ServiceLocator.Register<IInputService>(inputService);
  67. ServiceLocator.Register<ITimeService>(timeService);
  68. ServiceLocator.Register<IAudioService>(audioService);
  69. ServiceLocator.Register<IPhysicsProvider>(physicsProvider);
  70. ServiceLocator.Register<IGraphicsProvider>(graphicsProvider);
  71. ServiceLocator.Register<Gameplay.Systems.ProjectileSystem>(projectileSystem);
  72. // Initialize graphics provider with our GraphicsDevice
  73. graphicsProvider.SetGraphicsDevice(GraphicsDevice);
  74. // Create scene manager
  75. _sceneManager = new SceneManager();
  76. // TODO: Register custom component types
  77. // _sceneManager.RegisterComponentType<PlayerController>("PlayerController");
  78. base.Initialize();
  79. Console.WriteLine("MonoGame FPS initialized!");
  80. Console.WriteLine("Phase 1 services registered:");
  81. Console.WriteLine(" - InputService (keyboard, mouse, gamepad)");
  82. Console.WriteLine(" - TimeService (delta time, time scaling)");
  83. Console.WriteLine(" - AudioService (placeholder for Phase 4)");
  84. Console.WriteLine(" - BepuPhysicsProvider (multi-threaded physics)");
  85. Console.WriteLine(" - ForwardGraphicsProvider (BasicEffect rendering)");
  86. }
  87. /// <summary>
  88. /// Create a simple test scene for Phase 1 validation.
  89. /// This demonstrates:
  90. /// - Camera setup
  91. /// - Static and dynamic physics bodies
  92. /// - Mesh rendering with different colors
  93. /// - Basic 3D scene composition
  94. ///
  95. /// PHASE 2 UPDATE:
  96. /// - Player entity with FirstPersonController for movement
  97. /// - Mouse look and WASD controls enabled
  98. /// </summary>
  99. private void CreateTestScene()
  100. {
  101. // Create a new scene
  102. var scene = new Core.Scenes.Scene("Phase2TestScene");
  103. _sceneManager!.UnloadScene();
  104. // Create Player Entity with FPS Controller
  105. var playerEntity = new Core.Entities.Entity("Player");
  106. playerEntity.Tag = "Player";
  107. var playerTransform = playerEntity.AddComponent<Core.Components.Transform3D>();
  108. playerTransform.Position = new System.Numerics.Vector3(0, 2, 10); // Start above ground, back from origin
  109. // Add Camera to player
  110. var camera = playerEntity.AddComponent<Core.Components.Camera>();
  111. camera.FieldOfView = 75f;
  112. camera.NearPlane = 0.1f;
  113. camera.FarPlane = 100f;
  114. // Add FirstPersonController for movement (kinematic movement, no physics yet)
  115. var fpsController = playerEntity.AddComponent<Gameplay.Components.FirstPersonController>();
  116. fpsController.MoveSpeed = 50.0f; // Tuned for comfortable movement
  117. fpsController.MouseSensitivity = 50.0f; // Higher sensitivity for normalized delta (pixel delta normalized to -1 to 1 range)
  118. fpsController.JumpForce = 8.0f;
  119. // Add WeaponController for weapon management
  120. var weaponController = playerEntity.AddComponent<Gameplay.Components.WeaponController>();
  121. // Add Health to player
  122. var playerHealth = playerEntity.AddComponent<Gameplay.Components.Health>();
  123. playerHealth.MaxHealth = 100f;
  124. // Add HUD component to player
  125. var hud = playerEntity.AddComponent<Gameplay.Components.HUD>();
  126. scene.AddEntity(playerEntity);
  127. // Initialize camera AFTER entity is added and components are initialized
  128. camera.Position = playerTransform.Position;
  129. camera.Target = new System.Numerics.Vector3(0, 2, 0); // Look toward origin at same height
  130. // Equip weapons for testing
  131. var pistol = Gameplay.Weapons.Gun.CreatePistol();
  132. var rifle = Gameplay.Weapons.Gun.CreateAssaultRifle();
  133. weaponController.EquipWeapon(pistol, 0); // Slot 1 (press '1' to select)
  134. weaponController.EquipWeapon(rifle, 1); // Slot 2 (press '2' to select)
  135. Console.WriteLine("[Phase 2 Test Scene] Player created with FirstPersonController");
  136. Console.WriteLine(" Controls: WASD = Move, Mouse = Look, Space = Jump, Shift = Sprint, Escape = Release Mouse");
  137. Console.WriteLine(" Weapons: 1 = Pistol, 2 = Assault Rifle, LMB = Fire, R = Reload");
  138. // Create Ground Plane (large flat box)
  139. var groundEntity = new Core.Entities.Entity("Ground");
  140. var groundTransform = groundEntity.AddComponent<Core.Components.Transform3D>();
  141. groundTransform.Position = new System.Numerics.Vector3(0, -1, 0);
  142. groundTransform.LocalScale = new System.Numerics.Vector3(20, 0.5f, 20); // Wide and flat
  143. var groundRenderer = groundEntity.AddComponent<Core.Components.MeshRenderer>();
  144. groundRenderer.SetCube(1.0f, new System.Numerics.Vector4(0.3f, 0.5f, 0.3f, 1.0f)); // Green ground
  145. Console.WriteLine($"[DEBUG] Ground cube material color: {groundRenderer.Material.Color}");
  146. var groundRigid = groundEntity.AddComponent<Core.Components.Rigidbody>();
  147. groundRigid.BodyType = Core.Plugins.Physics.BodyType.Static;
  148. groundRigid.Shape = new Core.Plugins.Physics.BoxShape(20, 0.5f, 20);
  149. scene.AddEntity(groundEntity);
  150. // Create a few colored cubes (larger for easier targeting)
  151. CreateCube(scene, "RedCube", new System.Numerics.Vector3(-3, 2, 0), 2.0f, Color.Red);
  152. CreateCube(scene, "BlueCube", new System.Numerics.Vector3(0, 4, 0), 2.0f, Color.Blue);
  153. CreateCube(scene, "YellowCube", new System.Numerics.Vector3(3, 3, 0), 2.0f, Color.Yellow);
  154. // Create enemy entities for AI testing (bright red, taller/skinnier to distinguish from cubes)
  155. CreateEnemy(scene, playerEntity, "Enemy1", new System.Numerics.Vector3(-5, 2, -5), 1.0f, 0.0f, 0.0f);
  156. CreateEnemy(scene, playerEntity, "Enemy2", new System.Numerics.Vector3(5, 2, -5), 1.0f, 0.0f, 0.0f);
  157. // Initialize the scene
  158. scene.Initialize();
  159. Console.WriteLine($"[DEBUG] Scene has {scene.Entities.Count} entities after initialization");
  160. foreach (var ent in scene.Entities)
  161. {
  162. Console.WriteLine($"[DEBUG] - {ent.Name}: Active={ent.Active}, ComponentCount={ent.GetType().GetProperty("Components")?.GetValue(ent)}");
  163. }
  164. // Make it the active scene (hacky but works for testing)
  165. var activeSceneField = typeof(SceneManager).GetField("_activeScene",
  166. System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
  167. activeSceneField?.SetValue(_sceneManager, scene);
  168. Console.WriteLine("[Phase 2 Test Scene] Created:");
  169. Console.WriteLine(" - Player with FPS controller, weapons, health (100 HP), and HUD at (0, 2, 10)");
  170. Console.WriteLine(" - Ground plane (20x0.5x20 static box)");
  171. Console.WriteLine(" - 3 colored cubes with Health (50 HP each) for target practice");
  172. Console.WriteLine(" - 2 enemy entities with AI (20 HP, 10 damage melee attacks)");
  173. Console.WriteLine(" - Controls: WASD=Move, Mouse=Look, Space=Jump, Shift=Sprint, Esc=Menu");
  174. Console.WriteLine(" - Weapons: 1=Pistol, 2=Assault Rifle, LMB=Fire, R=Reload");
  175. }
  176. /// <summary>
  177. /// Helper to create a cube entity.
  178. /// </summary>
  179. private void CreateCube(Core.Scenes.Scene scene, string name,
  180. System.Numerics.Vector3 position, float size, Color color)
  181. {
  182. var entity = new Core.Entities.Entity(name);
  183. var transform = entity.AddComponent<Core.Components.Transform3D>();
  184. transform.Position = position;
  185. transform.LocalScale = new System.Numerics.Vector3(size);
  186. var renderer = entity.AddComponent<Core.Components.MeshRenderer>();
  187. renderer.SetCube(1.0f, new System.Numerics.Vector4(color.R / 255f, color.G / 255f, color.B / 255f, 1.0f));
  188. var rigidbody = entity.AddComponent<Core.Components.Rigidbody>();
  189. rigidbody.BodyType = Core.Plugins.Physics.BodyType.Static; // Static so cubes don't fall
  190. rigidbody.Shape = new Core.Plugins.Physics.BoxShape(size, size, size);
  191. rigidbody.Mass = 1.0f;
  192. // Add Health component for damage testing
  193. var health = entity.AddComponent<Gameplay.Components.Health>();
  194. health.MaxHealth = 50f;
  195. health.DestroyOnDeath = false; // Keep cube visible after death for testing
  196. // Subscribe to death event
  197. health.OnDeath += (damageInfo) =>
  198. {
  199. Console.WriteLine($"[Test Scene] {name} has been destroyed!");
  200. };
  201. scene.AddEntity(entity);
  202. }
  203. /// <summary>
  204. /// Helper to create an enemy entity with AI.
  205. /// </summary>
  206. private void CreateEnemy(Core.Scenes.Scene scene, Core.Entities.Entity player, string name,
  207. System.Numerics.Vector3 position, float r, float g, float b)
  208. {
  209. var entity = new Core.Entities.Entity(name);
  210. entity.Tag = "Enemy";
  211. var transform = entity.AddComponent<Core.Components.Transform3D>();
  212. transform.Position = position;
  213. transform.LocalScale = new System.Numerics.Vector3(0.8f, 1.8f, 0.8f); // Taller, skinnier - more humanoid
  214. var renderer = entity.AddComponent<Core.Components.MeshRenderer>();
  215. renderer.SetCube(1.0f, new System.Numerics.Vector4(r, g, b, 1.0f));
  216. var rigidbody = entity.AddComponent<Core.Components.Rigidbody>();
  217. rigidbody.BodyType = Core.Plugins.Physics.BodyType.Static; // Static for now (Phase 3 will add dynamic movement)
  218. rigidbody.Shape = new Core.Plugins.Physics.BoxShape(0.8f, 1.8f, 0.8f);
  219. rigidbody.Mass = 70.0f; // Human-like weight
  220. // Add Health component
  221. var health = entity.AddComponent<Gameplay.Components.Health>();
  222. health.MaxHealth = 20f;
  223. health.DestroyOnDeath = false; // Keep visible after death for testing
  224. // Subscribe to death event
  225. health.OnDeath += (damageInfo) =>
  226. {
  227. Console.WriteLine($"[Test Scene] {name} was killed by {damageInfo.Attacker?.Name ?? "unknown"}!");
  228. };
  229. // Add EnemyAI component
  230. var enemyAI = entity.AddComponent<Gameplay.Components.EnemyAI>();
  231. enemyAI.Target = player; // Set player as target
  232. enemyAI.DetectionRange = 15f;
  233. enemyAI.AttackRange = 2.5f;
  234. enemyAI.MoveSpeed = 2.5f;
  235. enemyAI.AttackDamage = 10f;
  236. enemyAI.AttackCooldown = 2.0f;
  237. scene.AddEntity(entity);
  238. Console.WriteLine($"[Test Scene] Created enemy '{name}' at {position} targeting player");
  239. }
  240. /// <summary>
  241. /// Load content (textures, models, sounds).
  242. /// Similar to Unity's resource loading but more manual.
  243. /// </summary>
  244. protected override void LoadContent()
  245. {
  246. _spriteBatch = new SpriteBatch(GraphicsDevice);
  247. // TODO Phase 1: Load content via Content Pipeline
  248. // TODO Phase 5: Load Gum UI screens
  249. Console.WriteLine("Content loaded (placeholder - Phase 1 will add actual content)");
  250. // Create Phase 2 test scene AFTER all services are initialized
  251. CreateTestScene();
  252. // Load HUD content after scene is created
  253. var playerEntity = _sceneManager?.ActiveScene?.FindEntitiesByTag("Player").FirstOrDefault();
  254. if (playerEntity != null)
  255. {
  256. var hud = playerEntity.GetComponent<Gameplay.Components.HUD>();
  257. hud?.LoadContent(GraphicsDevice, _spriteBatch);
  258. Console.WriteLine("[Game] HUD content loaded");
  259. }
  260. }
  261. /// <summary>
  262. /// Update game logic.
  263. /// This is called every frame, similar to Update() in Unity.
  264. ///
  265. /// EDUCATIONAL NOTE - Fixed Timestep Physics:
  266. /// We run physics on a fixed timestep (60 updates/sec) while
  267. /// rendering runs at variable framerate. This ensures:
  268. /// - Deterministic physics simulation
  269. /// - Consistent behavior across different hardware
  270. /// - Decoupled physics from rendering performance
  271. /// Unity does this automatically. In MonoGame we manage it manually.
  272. /// </summary>
  273. /// <param name="gameTime">Provides timing information</param>
  274. protected override void Update(GameTime gameTime)
  275. {
  276. // Update core services
  277. var timeService = ServiceLocator.Get<ITimeService>();
  278. var inputService = ServiceLocator.Get<IInputService>();
  279. var physicsProvider = ServiceLocator.Get<IPhysicsProvider>();
  280. timeService.Update(gameTime);
  281. inputService.Update();
  282. // Update active scene
  283. var coreGameTime = new Core.Components.GameTime(
  284. gameTime.TotalGameTime,
  285. gameTime.ElapsedGameTime
  286. );
  287. _sceneManager?.Update(coreGameTime);
  288. // Step physics simulation with fixed timestep
  289. // We use the scaled delta time from TimeService to support slow-motion/time effects
  290. // Guard against zero/negative timestep on first frame
  291. if (timeService.DeltaTime > 0)
  292. {
  293. physicsProvider.Step(timeService.DeltaTime);
  294. }
  295. base.Update(gameTime);
  296. }
  297. /// <summary>
  298. /// Draw/render the game.
  299. /// MonoGame requires you to explicitly handle rendering.
  300. /// Unity does this automatically based on cameras.
  301. ///
  302. /// EDUCATIONAL NOTE - Rendering Pipeline:
  303. /// 1. Clear backbuffer (prevent artifacts from previous frame)
  304. /// 2. BeginFrame - Set up render state
  305. /// 3. RenderScene - Draw all renderable entities
  306. /// 4. EndFrame - Present to screen
  307. /// Unity's rendering is automatic via Camera components.
  308. /// MonoGame gives you full control (and responsibility).
  309. /// </summary>
  310. /// <param name="gameTime">Provides timing information</param>
  311. protected override void Draw(GameTime gameTime)
  312. {
  313. GraphicsDevice.Clear(Color.CornflowerBlue);
  314. // Get graphics provider
  315. var graphics = ServiceLocator.Get<IGraphicsProvider>();
  316. // Begin frame
  317. graphics.BeginFrame();
  318. // Find active camera in scene (attached to Player entity in Phase 2)
  319. // TODO: Improve camera selection (support multiple cameras, camera priorities)
  320. Core.Components.Camera? camera = null;
  321. // Try to find camera on Player entity first
  322. var playerEntity = _sceneManager?.ActiveScene?.FindEntitiesByTag("Player").FirstOrDefault();
  323. if (playerEntity != null)
  324. {
  325. camera = playerEntity.GetComponent<Core.Components.Camera>();
  326. }
  327. // Fallback to MainCamera tag if no player camera found
  328. if (camera == null)
  329. {
  330. var cameraEntity = _sceneManager?.ActiveScene?.FindEntitiesByTag("MainCamera").FirstOrDefault();
  331. camera = cameraEntity?.GetComponent<Core.Components.Camera>();
  332. }
  333. if (camera != null)
  334. {
  335. // Collect all renderable entities from the scene
  336. var renderables = new System.Collections.Generic.List<IRenderable>();
  337. if (_sceneManager?.ActiveScene != null)
  338. {
  339. foreach (var entity in _sceneManager.ActiveScene.Entities)
  340. {
  341. // MeshRenderer implements IRenderable
  342. var meshRenderer = entity.GetComponent<Core.Components.MeshRenderer>();
  343. if (meshRenderer != null)
  344. {
  345. renderables.Add(meshRenderer);
  346. }
  347. }
  348. }
  349. // Render the scene
  350. graphics.RenderScene(camera, renderables);
  351. }
  352. else
  353. {
  354. Console.WriteLine("[Render] ERROR: No camera found!");
  355. }
  356. // End frame (presents to screen)
  357. graphics.EndFrame();
  358. // Draw crosshair (simple 2D overlay)
  359. DrawCrosshair();
  360. // Draw scene entities (UI/debug overlay - Phase 5)
  361. var coreGameTime = new Core.Components.GameTime(
  362. gameTime.TotalGameTime,
  363. gameTime.ElapsedGameTime
  364. );
  365. _sceneManager?.Draw(coreGameTime);
  366. base.Draw(gameTime);
  367. }
  368. /// <summary>
  369. /// Draw a simple crosshair in the center of the screen.
  370. /// This helps with aiming when the mouse cursor is hidden.
  371. /// </summary>
  372. private void DrawCrosshair()
  373. {
  374. if (_spriteBatch == null) return;
  375. _spriteBatch.Begin();
  376. // Get screen center
  377. int centerX = _graphics.PreferredBackBufferWidth / 2;
  378. int centerY = _graphics.PreferredBackBufferHeight / 2;
  379. // Crosshair size
  380. int crosshairSize = 10;
  381. int thickness = 2;
  382. int gap = 3;
  383. // Create a 1x1 white pixel texture for drawing lines
  384. Texture2D pixel = new Texture2D(GraphicsDevice, 1, 1);
  385. pixel.SetData(new[] { Color.White });
  386. // Draw horizontal line (left and right from center)
  387. _spriteBatch.Draw(pixel, new Rectangle(centerX - crosshairSize - gap, centerY - thickness / 2, crosshairSize, thickness), Color.White);
  388. _spriteBatch.Draw(pixel, new Rectangle(centerX + gap, centerY - thickness / 2, crosshairSize, thickness), Color.White);
  389. // Draw vertical line (top and bottom from center)
  390. _spriteBatch.Draw(pixel, new Rectangle(centerX - thickness / 2, centerY - crosshairSize - gap, thickness, crosshairSize), Color.White);
  391. _spriteBatch.Draw(pixel, new Rectangle(centerX - thickness / 2, centerY + gap, thickness, crosshairSize), Color.White);
  392. _spriteBatch.End();
  393. pixel.Dispose();
  394. }
  395. /// <summary>
  396. /// Cleanup when game exits.
  397. /// Similar to Unity's OnApplicationQuit() or OnDestroy().
  398. /// </summary>
  399. protected override void UnloadContent()
  400. {
  401. // Unload scene
  402. _sceneManager?.UnloadScene();
  403. // Shutdown services
  404. ServiceLocator.Get<IPhysicsProvider>()?.Shutdown();
  405. ServiceLocator.Get<IGraphicsProvider>()?.Shutdown();
  406. ServiceLocator.Clear();
  407. Console.WriteLine("MonoGame FPS shutdown complete");
  408. base.UnloadContent();
  409. }
  410. }