diff --git a/src/Client/GameModel.cs b/src/Client/GameModel.cs index c4cbf30..c652310 100644 --- a/src/Client/GameModel.cs +++ b/src/Client/GameModel.cs @@ -157,6 +157,12 @@ private Entity createEntity(Shared.Messages.NewEntity message) } // Worm parts + + if (message.hasWorm) + { + entity.add(new Worm()); + entity.add(new AnchorQueue()); // We implicitly need this because every worm part has it + } if (message.hasHead) { @@ -177,12 +183,6 @@ private Entity createEntity(Shared.Messages.NewEntity message) { entity.add(new ChildId(message.childId)); } - - if (message.hasWorm) - { - entity.add(new Worm()); - entity.add(new AnchorQueue()); // We implicitly need this because every worm part has it - } if (message.hasInvincible) { diff --git a/src/Client/Menu/CreditsView.cs b/src/Client/Menu/CreditsView.cs index c0e9838..ddad9ce 100644 --- a/src/Client/Menu/CreditsView.cs +++ b/src/Client/Menu/CreditsView.cs @@ -9,7 +9,7 @@ namespace Client.Menu public class AboutView : GameStateView { private SpriteFont m_font; - private const string MESSAGE = "Created by Caden, Max, and Satchel in 2024. Enjoy!"; + private const string MESSAGE = "Created by\nCaden, Max, and Satchel.\n2024 - CS 5410\nEnjoy!"; private bool isKeyboardRegistered = false; private MenuStateEnum newState = MenuStateEnum.Credits; public override void loadContent(ContentManager contentManager) @@ -38,7 +38,7 @@ public override MenuStateEnum processInput(GameTime gameTime) public override void render(GameTime gameTime) { m_spriteBatch.Begin(); - Drawing.CustomDrawString(m_font, MESSAGE, new Vector2(m_graphics.PreferredBackBufferWidth / 2, m_graphics.PreferredBackBufferHeight / 2), Colors.displayColor ,m_spriteBatch); + Drawing.CustomDrawString(m_font, MESSAGE, new Vector2(m_graphics.PreferredBackBufferWidth / 2, m_graphics.PreferredBackBufferHeight / 2), Colors.displayColor ,m_spriteBatch, boxed:true); m_spriteBatch.End(); } diff --git a/src/Client/Menu/HelpView.cs b/src/Client/Menu/HelpView.cs index 4b8ae7e..3b7cd05 100644 --- a/src/Client/Menu/HelpView.cs +++ b/src/Client/Menu/HelpView.cs @@ -9,7 +9,7 @@ namespace Client.Menu public class HelpView : GameStateView { private SpriteFont m_font; - private const string MESSAGE = "Eat food to grow.\nAvoid other sandworms until you are large enough to consume them.\nUse the arrow keys to move and press escape to return to the main menu.\nGood Luck!"; + private const string MESSAGE = "Eat food to grow.\nAvoid other sandworms until\nyou are large enough to consume them.\nUse the arrow keys to move\nand press escape to return to the main menu.\nGood Luck!"; private bool isKeyboardRegistered = false; private MenuStateEnum newState = MenuStateEnum.Help; diff --git a/src/Client/Menu/HighScoresView.cs b/src/Client/Menu/HighScoresView.cs index fd231cb..5e45ee1 100644 --- a/src/Client/Menu/HighScoresView.cs +++ b/src/Client/Menu/HighScoresView.cs @@ -6,6 +6,7 @@ using System; using System.Linq; using Client.IO; +using Client.Systems; using Shared.Components; using Shared.Entities; using Shared.Systems; diff --git a/src/Client/Menu/HowToPlay.cs b/src/Client/Menu/HowToPlay.cs index d191b41..ec62f5e 100644 --- a/src/Client/Menu/HowToPlay.cs +++ b/src/Client/Menu/HowToPlay.cs @@ -74,7 +74,7 @@ public override void render(GameTime gameTime) // Background Rectangle var recPosition = new Vector2(m_graphics.PreferredBackBufferWidth / 5 - 20, m_graphics.PreferredBackBufferHeight / 4 - 50); - Drawing.DrawBlurredRectangle(m_spriteBatch, recPosition, new Vector2(700, 400), 5, 0.6f); + Drawing.DrawBlurredRectangle(m_spriteBatch, recPosition, new Vector2(700, 400), 5, 0.9f); // Title diff --git a/src/Client/Menu/MainMenuView.cs b/src/Client/Menu/MainMenuView.cs index 735ce47..7fab1e2 100644 --- a/src/Client/Menu/MainMenuView.cs +++ b/src/Client/Menu/MainMenuView.cs @@ -65,6 +65,7 @@ public override void render(GameTime gameTime) renderSound = false; // reset the flag } // I split the first one's parameters on separate lines to help you see them better + Drawing.DrawBlurredRectangle(m_spriteBatch, new Vector2(m_graphics.PreferredBackBufferWidth / 4, 150), new Vector2(m_graphics.PreferredBackBufferWidth / 2, 500), 5 ); float bottom = drawMenuItem( m_currentSelection == MenuState.NewGame ? m_fontMenuSelect : m_fontMenu, "New Game", diff --git a/src/Client/Menu/Styles.cs b/src/Client/Menu/Styles.cs index 0ee2604..ae1c29c 100644 --- a/src/Client/Menu/Styles.cs +++ b/src/Client/Menu/Styles.cs @@ -14,7 +14,7 @@ public static void CustomDrawString(SpriteFont font, string message, Vector2 pos position = new Vector2(position.X - font.MeasureString(message).X / 2, position.Y - font.MeasureString(message).Y / 2); } if (boxed) { - DrawBlurredRectangle(spriteBatch, position, font.MeasureString(message), 5, 0.6f); + DrawBlurredRectangle(spriteBatch, position, font.MeasureString(message), 5); } if (shaded) { for (int i = 1; i < 3; i++) { @@ -24,7 +24,7 @@ public static void CustomDrawString(SpriteFont font, string message, Vector2 pos spriteBatch.DrawString(font, message, position, color, 0, Vector2.Zero, scale, SpriteEffects.None, 0); } - public static void DrawBlurredRectangle(SpriteBatch spriteBatch, Vector2 position, Vector2 size, int blurRadius, float transparency = 0.8f) + public static void DrawBlurredRectangle(SpriteBatch spriteBatch, Vector2 position, Vector2 size, int blurRadius, float transparency = 0.85f) { Rectangle blurredRect = new Rectangle((int)(position.X - blurRadius), (int)(position.Y - blurRadius), (int)size.X + blurRadius * 2, (int)size.Y + blurRadius * 2); Color color = Color.Black * transparency; // Adjust the color and transparency as needed diff --git a/src/Shared/Systems/GameScoresPersistence.cs b/src/Client/Systems/GameScoresPersistence.cs similarity index 97% rename from src/Shared/Systems/GameScoresPersistence.cs rename to src/Client/Systems/GameScoresPersistence.cs index b9828e9..d1666e4 100644 --- a/src/Shared/Systems/GameScoresPersistence.cs +++ b/src/Client/Systems/GameScoresPersistence.cs @@ -1,9 +1,12 @@ +using System.IO; using System.IO.IsolatedStorage; using System.Runtime.Serialization.Json; +using System.Threading.Tasks; using Shared.Components; using Shared.Entities; +using Client.Components; -namespace Shared.Systems; +namespace Client.Systems; public class GameScoresPersistence { diff --git a/src/Client/Systems/KeyboardInput.cs b/src/Client/Systems/KeyboardInput.cs index f3974ab..40c8675 100644 --- a/src/Client/Systems/KeyboardInput.cs +++ b/src/Client/Systems/KeyboardInput.cs @@ -15,7 +15,8 @@ public class KeyboardInput : Shared.Systems.System private KeyboardState m_statePrevious = Keyboard.GetState(); private Controls m_controls = new Controls(); // Default value that is overwritten in the constructor - public KeyboardInput(List> mapping, Controls controls) : base(typeof(Shared.Components.Worm)) + public KeyboardInput(List> mapping, Controls controls) : base( + typeof(Shared.Components.Worm)) { m_controls = controls; } @@ -28,6 +29,7 @@ public override void update(TimeSpan elapsedTime) { m_keysPressed.Add(key); } + // We have a dictionary of entities, so we need to iterate through them foreach (var entity in m_entities) { @@ -35,62 +37,73 @@ public override void update(TimeSpan elapsedTime) { continue; } + var inputs = new List(); var worm = WormMovement.getWormFromHead(entity.Value, m_entities); // Start with the combinations - if (keyPressed(m_controls.SnakeLeft.key) && keyPressed(m_controls.SnakeUp.key)) + if (keysNewlyPressed(m_controls.SnakeUp.key, m_controls.SnakeLeft.key)) { - inputs.Add(Input.Type.PointUpLeft); + inputs.Add(Input.Type.PointUpLeft); Shared.Systems.WormMovement.upLeft(worm); - } else if (keyPressed(m_controls.SnakeRight.key) && keyPressed(m_controls.SnakeUp.key)) + } + else if (keysNewlyPressed( m_controls.SnakeUp.key, m_controls.SnakeRight.key )) { inputs.Add(Input.Type.PointUpRight); Shared.Systems.WormMovement.upRight(worm); - } else if (keyPressed(m_controls.SnakeLeft.key) && keyPressed(m_controls.SnakeDown.key)) + } + else if (keysNewlyPressed( m_controls.SnakeDown.key, m_controls.SnakeLeft.key )) { inputs.Add(Input.Type.PointDownLeft); Shared.Systems.WormMovement.downLeft(worm); - } else if (keyPressed(m_controls.SnakeRight.key) && keyPressed(m_controls.SnakeDown.key)) + } + else if (keysNewlyPressed( m_controls.SnakeDown.key, m_controls.SnakeRight.key )) { inputs.Add(Input.Type.PointDownRight); Shared.Systems.WormMovement.downRight(worm); - } else if (keyNewlyPressed(m_controls.SnakeLeft.key)) + } + else if (keyNewlyPressed(m_controls.SnakeLeft.key)) { inputs.Add(Input.Type.PointLeft); Shared.Systems.WormMovement.left(worm, elapsedTime); - }else if (keyNewlyPressed(m_controls.SnakeRight.key)) + } + else if (keyNewlyPressed(m_controls.SnakeRight.key)) { inputs.Add(Input.Type.PointRight); Shared.Systems.WormMovement.right(worm, elapsedTime); - }else if (keyNewlyPressed(m_controls.SnakeUp.key)) + } + else if (keyNewlyPressed(m_controls.SnakeUp.key)) { inputs.Add(Input.Type.PointUp); Shared.Systems.WormMovement.up(worm); - }else if (keyNewlyPressed(m_controls.SnakeDown.key)) + } + else if (keyNewlyPressed(m_controls.SnakeDown.key)) { inputs.Add(Input.Type.PointDown); Shared.Systems.WormMovement.down(worm); } - - + + if (inputs.Count > 0) { // Assuming you have a messaging system to handle input - MessageQueueClient.instance.sendMessageWithId(new Shared.Messages.Input(entity.Key, inputs, elapsedTime)); + MessageQueueClient.instance.sendMessageWithId(new Shared.Messages.Input(entity.Key, inputs, + elapsedTime)); } } + // Move the current state to the previous state for the next time around m_statePrevious = keyboardState; } - - + + public override bool add(Entity entity) { if (!base.add(entity)) { return false; } + return true; } @@ -98,7 +111,7 @@ public override void remove(uint id) { base.remove(id); } - + /// /// Checks to see if a key was newly pressed /// @@ -111,5 +124,13 @@ private bool keyPressed(Keys key) { return m_keysPressed.Contains(key); } + + private bool keysNewlyPressed(Keys key1, Keys key2) + { + return keyPressed(key1) && keyPressed(key2) && (!m_statePrevious.IsKeyDown(key1) || + !m_statePrevious.IsKeyDown(key2)); + } } } + + diff --git a/src/Client/Systems/Network.cs b/src/Client/Systems/Network.cs index 1faf8c0..87970b8 100644 --- a/src/Client/Systems/Network.cs +++ b/src/Client/Systems/Network.cs @@ -97,7 +97,6 @@ public void update(TimeSpan elapsedTime, Queue messages) } } } - // After processing all the messages, perform server reconciliation by // resimulating the inputs from any sent messages not yet acknowledged by the server. @@ -106,7 +105,7 @@ public void update(TimeSpan elapsedTime, Queue messages) while (sent.Count > 0) { var message = (Shared.Messages.Input)sent.Dequeue(); - if (message.type == Shared.Messages.Type.Input) + if (message.type == Shared.Messages.Type.Input && m_entities.ContainsKey(message.entityId)) { var entity = m_entities[message.entityId]; Debug.Assert(entity.contains()); diff --git a/src/Client/Systems/Renderer.cs b/src/Client/Systems/Renderer.cs index 9bed68d..a873710 100644 --- a/src/Client/Systems/Renderer.cs +++ b/src/Client/Systems/Renderer.cs @@ -133,7 +133,6 @@ private void renderEntity(TimeSpan elapsedTime, SpriteBatch spriteBatch, Entity if (entity.contains()) { - var invincible = entity.get(); color = Color.Coral; } diff --git a/src/Server/Systems/Network.cs b/src/Server/Systems/Network.cs index 89d8548..13b3b39 100644 --- a/src/Server/Systems/Network.cs +++ b/src/Server/Systems/Network.cs @@ -147,6 +147,7 @@ private void handleInput(Shared.Messages.Input message) } if (update) { + // m_reportThese.Add(message.entityId); foreach (var e in worm) { m_reportThese.Add(e.id); diff --git a/src/Shared/Systems/WormMovement.cs b/src/Shared/Systems/WormMovement.cs index ce311cb..e94539b 100644 --- a/src/Shared/Systems/WormMovement.cs +++ b/src/Shared/Systems/WormMovement.cs @@ -34,7 +34,6 @@ public override void update(TimeSpan elapsedTime) } } } - } private List getHeads() @@ -51,47 +50,79 @@ private List getHeads() return heads; } + // Core 4 directions + private static float RIGHT_Radians = 0; private static float UP_Radians = -MathHelper.PiOver2; private static float DOWN_Radians = MathHelper.PiOver2; + private static float LEFT_Radians = MathHelper.Pi; + // Diagonal 4 directions + private static float UP_RIGHT_Radians = -MathHelper.PiOver4; + private static float UP_LEFT_Radians = -3 * MathHelper.PiOver4; + private static float DOWN_RIGHT_Radians = MathHelper.PiOver4; + private static float DOWN_LEFT_Radians = 3 * MathHelper.PiOver4; - public static void upLeft(List snake) + + public static void up(List snake) { - changeDirection(snake, UP_Radians - MathHelper.PiOver4); + if (!isWithinAngleThreshold(snake[0].get().orientation, DOWN_Radians)) + { + changeDirection(snake, UP_Radians); + } } - public static void upRight(List snake) + public static void down(List snake) { - changeDirection(snake, UP_Radians + MathHelper.PiOver4); + if (!isWithinAngleThreshold(snake[0].get().orientation, UP_Radians)) + { + changeDirection(snake, DOWN_Radians); + } } - public static void downRight(List snake) + public static void left(List snake, TimeSpan elapsedTime) { - changeDirection(snake, DOWN_Radians - MathHelper.PiOver4); + if (!isWithinAngleThreshold(snake[0].get().orientation, RIGHT_Radians)) + { + changeDirection(snake, LEFT_Radians); + } } - - public static void downLeft(List snake) + + public static void right(List snake, TimeSpan elapsedTime) { - changeDirection(snake, DOWN_Radians + MathHelper.PiOver4); + if (!isWithinAngleThreshold(snake[0].get().orientation, LEFT_Radians)) + { + changeDirection(snake, RIGHT_Radians); + } } - - public static void left(List snake, TimeSpan elapsedTime) + public static void upLeft(List snake) { - changeDirection(snake, MathHelper.Pi); + if (!isWithinAngleThreshold( snake[0].get().orientation, DOWN_RIGHT_Radians)) + { + changeDirection(snake, UP_LEFT_Radians); + } } - public static void up(List snake) + public static void upRight(List snake) { - changeDirection(snake, UP_Radians); + if (!isWithinAngleThreshold(snake[0].get().orientation, DOWN_LEFT_Radians)) + { + changeDirection(snake, UP_RIGHT_Radians); + } } - public static void down(List snake) + public static void downLeft(List snake) { - changeDirection(snake, DOWN_Radians); + if (!isWithinAngleThreshold(snake[0].get().orientation, UP_RIGHT_Radians)) + { + changeDirection(snake, DOWN_LEFT_Radians); + } } - - public static void right(List snake, TimeSpan elapsedTime) + + public static void downRight(List snake) { - changeDirection(snake, MathHelper.TwoPi); + if (!isWithinAngleThreshold(snake[0].get().orientation, UP_LEFT_Radians)) + { + changeDirection(snake, DOWN_RIGHT_Radians); + } } private void applyThrust(Entity wormHead, TimeSpan elapsedTime) @@ -101,17 +132,16 @@ private void applyThrust(Entity wormHead, TimeSpan elapsedTime) var head = snake[0]; var movement = head.get(); var headPosition = head.get(); - var frameTotalMovement = movement.moveRate * (float)elapsedTime.TotalMilliseconds; var orientation = headPosition.orientation; float LOCATION_THRESHOLD = movement.moveRate * 20; const float MIN_SEGMENT_SPACING = 40f; const float IDEAL_SEGMENT_SPACING = 50f; - - + + // Move the head var direction = new Vector2((float)Math.Cos(orientation), (float)Math.Sin(orientation)); direction.Normalize(); - headPosition.position += direction * frameTotalMovement; + headPosition.position += direction * movement.moveRate * (float)elapsedTime.TotalMilliseconds;; // Move the rest of the worm for (int i = 1; i < snake.Count; i++) @@ -121,57 +151,71 @@ private void applyThrust(Entity wormHead, TimeSpan elapsedTime) var currentPosition = entity.get(); var parent = snake[i - 1]; var parentPosition = parent.get(); - + var entityFrameMovement = movement.moveRate * (float)elapsedTime.TotalMilliseconds; + // Default moving towards parent position var target = new Position(parentPosition.position, parentPosition.orientation); - if (queueComponent.m_anchorPositions.Count != 0) + while (queueComponent.m_anchorPositions.Count > 0 && entityFrameMovement > 0) { - // queueComponent.m_anchorPositions.Enqueue(new Position(parentPosition.position, parentPosition.orientation)); target = queueComponent.m_anchorPositions.Peek(); + var distanceToTarget = Vector2.Distance(currentPosition.position, target.position); + + // Move towards the target + if (distanceToTarget > entityFrameMovement) + { + var directionToTarget = target.position - currentPosition.position; + directionToTarget.Normalize(); + currentPosition.position += directionToTarget * entityFrameMovement; + entityFrameMovement = 0; + } + else + { + // Move to the target + currentPosition.position = target.position; + entityFrameMovement -= distanceToTarget; + queueComponent.m_anchorPositions.Dequeue(); + } } - // Move towards that target - var distanceToTarget = Vector2.Distance(currentPosition.position, target.position); - var distanceToParent = Vector2.Distance(currentPosition.position, parentPosition.position); - if (distanceToTarget >= MIN_SEGMENT_SPACING || distanceToParent >= IDEAL_SEGMENT_SPACING) + if (entityFrameMovement > 0) { - var directionToTarget = target.position - currentPosition.position; - directionToTarget.Normalize(); - currentPosition.position += directionToTarget * frameTotalMovement; - - // Update the orientation to match the direction we're moving - currentPosition.orientation = (float)Math.Atan2(directionToTarget.Y, directionToTarget.X); - - // Check if we have hit the target - if (Vector2.Distance(currentPosition.position, target.position) <= LOCATION_THRESHOLD && queueComponent.m_anchorPositions.Count > 0) + target = new Position(parentPosition.position, parentPosition.orientation); + var distanceToTarget = Vector2.Distance(currentPosition.position, target.position); + // We want to move towards the parent but not so close we are on top of it + if (distanceToTarget > IDEAL_SEGMENT_SPACING) { - // Remove the target from the queue - queueComponent.m_anchorPositions.Dequeue(); + var directionToParent = parentPosition.position - currentPosition.position; + directionToParent.Normalize(); + currentPosition.position += directionToParent * entityFrameMovement; } } } } + private static bool isWithinAngleThreshold(float radians, float targetRadians, float threshold = MathHelper.PiOver4/2) + { + // We need to normalize the angles to ensure they are within the same range and both positive + radians = (radians + MathHelper.TwoPi) % MathHelper.TwoPi; + targetRadians = (targetRadians + MathHelper.TwoPi) % MathHelper.TwoPi; + var diff = Math.Abs(radians - targetRadians); + // If the difference is greater than PI, we need to use the complementary angle + if (diff > MathHelper.Pi) + { + diff = MathHelper.TwoPi - diff; + } + return diff < threshold; + } + private static void changeDirection(List worm, float radians) { - if (worm == null || worm.Count == 0) + if (worm == null || worm.Count == 0 || isWithinAngleThreshold(worm[0].get().orientation, radians)) return; // Assuming the first entity in the list is the head var head = worm[0]; var headPosition = head.get(); - - // Adjust the head's orientation by the specified radians and that is it not 180 - var oppositeDirction = (radians + MathHelper.Pi) % (2 * Math.PI); - var headIsOppositeDirection = Math.Abs(headPosition.orientation - oppositeDirction) < 0.1; - if (headPosition.orientation == radians || headIsOppositeDirection) return; headPosition.orientation = radians; - // Normalize the orientation to ensure it stays within a valid range (e.g., 0 to 2*PI) - // This step is important if your system expects orientations within a specific range - headPosition.orientation = (float)(headPosition.orientation % (2 * Math.PI)); - if (headPosition.orientation < 0) headPosition.orientation += (float)(2 * Math.PI); - // For each segment, add the current head position to its queue for following // Skip the head itself, start with the first segment following the head for (int i = 1; i < worm.Count; i++)