2
0

Game.cs 21 KB

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