diff --git a/Shared/FSM.cs b/Shared/FSM.cs index 42d0812..8791b09 100644 --- a/Shared/FSM.cs +++ b/Shared/FSM.cs @@ -1,20 +1,19 @@ using System.Collections.Generic; namespace SemiColinGames { - public interface IState { + public interface IState { // Called automatically whenever this state is transitioned to. Should reset whichever // state-specific variables need resetting. public void Enter(); // Returns the name of the new state, or null if we should stay in the same state. - public string Update(float modelTime, World world); + public string Update(float modelTime, World world, T input); } - public class FSM { - readonly Dictionary states; - IState state; + public class FSM { + readonly Dictionary> states; - public FSM(string initialStateName, Dictionary states) { + public FSM(string initialStateName, Dictionary> states) { this.states = states; StateName = initialStateName; Transition(StateName); @@ -22,8 +21,10 @@ namespace SemiColinGames { public string StateName { get; private set; } - public void Update(float modelTime, World world) { - string newState = state.Update(modelTime, world); + public IState State { get; private set; } + + public void Update(float modelTime, World world, T input) { + string newState = State.Update(modelTime, world, input); if (newState != null) { Transition(newState); } @@ -31,9 +32,9 @@ namespace SemiColinGames { void Transition(string state) { StateName = state; - IState newState = states[state]; - this.state = newState; - this.state.Enter(); + IState newState = states[state]; + State = newState; + State.Enter(); } } } diff --git a/Shared/NPC.cs b/Shared/NPC.cs index 1541be3..7e60de3 100644 --- a/Shared/NPC.cs +++ b/Shared/NPC.cs @@ -1,10 +1,11 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; +using System; using System.Collections.Generic; namespace SemiColinGames { - class IdleState : IState { - private NPC npc; + class IdleState : IState { + private readonly NPC npc; private float timeInState = 0; public IdleState(NPC npc) { @@ -15,7 +16,7 @@ namespace SemiColinGames { timeInState = 0; } - public string Update(float modelTime, World world) { + public string Update(float modelTime, World world, Object _) { timeInState += modelTime; if (timeInState > 1.0f) { npc.Facing *= -1; @@ -25,8 +26,8 @@ namespace SemiColinGames { } } - class RunState : IState { - private NPC npc; + class RunState : IState { + private readonly NPC npc; public RunState(NPC npc) { this.npc = npc; @@ -34,7 +35,7 @@ namespace SemiColinGames { public void Enter() {} - public string Update(float modelTime, World world) { + public string Update(float modelTime, World world, Object _) { float moveSpeed = 120; float desiredX = npc.Position.X + moveSpeed * npc.Facing * modelTime; float testPoint = desiredX + npc.Box.HalfSize.X * npc.Facing; @@ -63,7 +64,7 @@ namespace SemiColinGames { private readonly Vector2 spriteCenter; private readonly Vector2 eyeOffset = new Vector2(4, -9); - private readonly FSM fsm; + private readonly FSM fsm; private readonly Vector2 halfSize = new Vector2(11, 24); public NPC(Vector2 position, int facing) { @@ -75,7 +76,7 @@ namespace SemiColinGames { Box = new AABB(Position, halfSize); Facing = facing; - fsm = new FSM("run", new Dictionary { + fsm = new FSM("run", new Dictionary> { { "idle", new IdleState(this) }, { "run", new RunState(this) } }); @@ -110,7 +111,7 @@ namespace SemiColinGames { } public void Update(float modelTime, World world) { - fsm.Update(modelTime, world); + fsm.Update(modelTime, world, null); Box = new AABB(Position, halfSize); Debug.AddRect(Box, Color.White); } diff --git a/Shared/Player.cs b/Shared/Player.cs index 14ac866..a2a3f1b 100644 --- a/Shared/Player.cs +++ b/Shared/Player.cs @@ -4,13 +4,80 @@ using System; using System.Collections.Generic; namespace SemiColinGames { - public class Player { - private enum Pose { Walking, Standing, SwordSwing, Jumping }; - private const int moveSpeed = 180; + interface IPlayerState : IState> { + public Vector2 Movement { get; } + public void PostUpdate(bool standingOnGround); + } + + class StandState : IPlayerState { + private Vector2 result; + + // private double swordSwingTime = 0; + // private int swordSwingNum = 0; + private float ySpeed = 0; + private int jumps = 1; private const int jumpSpeed = -600; + private const int moveSpeed = 180; private const int gravity = 1600; + public void Enter() { + } + + public string Update(float modelTime, World world, History input) { + result = new Vector2() { + X = input[0].Motion.X * moveSpeed * modelTime + }; + + if (input[0].Jump && !input[1].Jump && jumps > 0) { + jumps--; + ySpeed = jumpSpeed; + } + + // if (input[0].Attack && !input[1].Attack && swordSwingTime <= 0) { + // swordSwingTime = 0.3; + // swordSwingNum++; + // SoundEffects.SwordSwings[swordSwingNum % SoundEffects.SwordSwings.Length].Play(); + // } + + result.Y = ySpeed * modelTime; + ySpeed += gravity * modelTime; + // swordSwingTime -= modelTime; + + if (input[0].IsAbsoluteMotion) { + if (input[1].Motion.X == 0) { + result.X = input[0].Motion.X; + } else { + result.X = 0; + } + } + + return null; + } + + public Vector2 Movement { + get { return result; } + } + + // TODO: Maybe this should be Update(), and CalculateMovement() should be the Player-specific + // function? + public void PostUpdate(bool standingOnGround) { + if (standingOnGround) { + jumps = 1; + ySpeed = -0.0001f; + // Debug.AddRect(Box(position), Color.Cyan); + } else { + jumps = 0; + // Debug.AddRect(Box(position), Color.Orange); + } + } + } + + public class Player { + private readonly FSM> fsm; + // TODO: get rid of Pose. + private enum Pose { Walking, Standing, SwordSwing, Jumping }; + // Details of the sprite image. // player_1x is 48 x 48, yOffset=5, halfSize=(7, 14) // Ninja_Female is 96 x 64, yOffset=1, halfSize=(11, 24) @@ -28,11 +95,7 @@ namespace SemiColinGames { // Useful so that we can run at a slow time-step and still get non-zero motion. private Vector2 residual = Vector2.Zero; - private int jumps = 0; private Pose pose = Pose.Jumping; - private double swordSwingTime = 0; - private int swordSwingNum = 0; - private float ySpeed = 0; private float invincibilityTime = 0; // For passing into Line.Rasterize() during movement updates. @@ -45,6 +108,9 @@ namespace SemiColinGames { Facing = facing; Health = MaxHealth; StandingOnGround = false; + fsm = new FSM>("run", new Dictionary>> { + { "run", new StandState() }, + }); } public bool StandingOnGround { get; private set; } @@ -68,7 +134,7 @@ namespace SemiColinGames { invincibilityTime -= modelTime; - Vector2 inputMovement = HandleInput(modelTime, input); + Vector2 inputMovement = HandleInput(modelTime, world, input); Vector2 movement = Vector2.Add(residual, inputMovement); residual = new Vector2(movement.X - (int) movement.X, movement.Y - (int) movement.Y); @@ -145,14 +211,7 @@ namespace SemiColinGames { } } - if (StandingOnGround) { - jumps = 1; - ySpeed = -0.0001f; - Debug.AddRect(Box(position), Color.Cyan); - } else { - jumps = 0; - Debug.AddRect(Box(position), Color.Orange); - } + ((IPlayerState) fsm.State).PostUpdate(StandingOnGround); if (harmedByCollision && invincibilityTime <= 0) { world.ScreenShake(); @@ -165,11 +224,7 @@ namespace SemiColinGames { } else if (inputMovement.X < 0) { Facing = -1; } - if (swordSwingTime > 0) { - pose = Pose.SwordSwing; - } else if (jumps == 0) { - pose = Pose.Jumping; - } else if (inputMovement.X != 0) { + if (inputMovement.X != 0) { pose = Pose.Walking; } else { pose = Pose.Standing; @@ -177,35 +232,10 @@ namespace SemiColinGames { } // Returns the desired (dx, dy) for the player to move this frame. - Vector2 HandleInput(float modelTime, History input) { - Vector2 result = new Vector2() { - X = input[0].Motion.X * moveSpeed * modelTime - }; - - if (input[0].Jump && !input[1].Jump && jumps > 0) { - jumps--; - ySpeed = jumpSpeed; - } - - if (input[0].Attack && !input[1].Attack && swordSwingTime <= 0) { - swordSwingTime = 0.3; - swordSwingNum++; - SoundEffects.SwordSwings[swordSwingNum % SoundEffects.SwordSwings.Length].Play(); - } - - result.Y = ySpeed * modelTime; - ySpeed += gravity * modelTime; - swordSwingTime -= modelTime; - - if (input[0].IsAbsoluteMotion) { - if (input[1].Motion.X == 0) { - result.X = input[0].Motion.X; - } else { - result.X = 0; - } - } - - return result; + Vector2 HandleInput(float modelTime, World world, History input) { + fsm.Update(modelTime, world, input); + // TODO: remove ugly cast. + return ((IPlayerState) fsm.State).Movement; } private Rectangle GetTextureSource(Pose pose) { @@ -216,8 +246,8 @@ namespace SemiColinGames { return Sprites.Ninja.GetTextureSource("run", time); case Pose.SwordSwing: // TODO: make a proper animation class & FSM-driven animations. - return Sprites.Ninja.GetTextureSource( - "attack_sword", 0.3 - swordSwingTime); + //return Sprites.Ninja.GetTextureSource( + // "attack_sword", 0.3 - swordSwingTime); case Pose.Standing: default: return Sprites.Ninja.GetTextureSource("idle", time);