From a60b29a6b343d98da3a96450833bf71d4b567d10 Mon Sep 17 00:00:00 2001 From: Nik Cherednik Date: Wed, 20 Sep 2023 12:49:02 +0300 Subject: [PATCH] refactor: ecs & good workflow --- .../GameDesktop/RootFeatureCompositionRoot.cs | 47 +++++++--- .../GeneratedExtended/GameEntity.cs | 16 ++++ src/Libs/Components/Jenny.properties | 1 - .../Components/MovementAnimationComponent.cs | 31 +++++++ .../Sprites/AnimatedMovementComponent.cs | 91 ------------------- .../World/RectangleCollisionComponent.cs | 1 - .../Entitas.Extended/Entitas.Extended.csproj | 4 + .../Factories/AnimatedCharactersFactory.cs | 24 ++--- src/Libs/Services/Math/MathUtils.cs | 10 +- src/Libs/Systems/AnimatedMovementSystem.cs | 50 ++++++++-- src/Libs/Systems/CreatePlayerEntitySystem.cs | 27 +++--- src/UnitTests/UnitTests.Services/Rad8Dir.cs | 32 +++---- 12 files changed, 169 insertions(+), 165 deletions(-) create mode 100644 src/Libs/Components/GeneratedExtended/GameEntity.cs create mode 100644 src/Libs/Components/MovementAnimationComponent.cs delete mode 100644 src/Libs/Components/Sprites/AnimatedMovementComponent.cs diff --git a/src/Apps/GameDesktop/RootFeatureCompositionRoot.cs b/src/Apps/GameDesktop/RootFeatureCompositionRoot.cs index 11a2e10..f73d1cc 100644 --- a/src/Apps/GameDesktop/RootFeatureCompositionRoot.cs +++ b/src/Apps/GameDesktop/RootFeatureCompositionRoot.cs @@ -1,4 +1,5 @@ -using Components.Sprites; +using Components; +using Components.World; using Entitas; using Features; using LightInject; @@ -9,7 +10,6 @@ using Services.Factories; using Services.Input; using Services.Movement; -using Stateless; using Systems; namespace GameDesktop; @@ -17,11 +17,25 @@ namespace GameDesktop; public class RootFeatureCompositionRoot : ICompositionRoot { public void Compose(IServiceRegistry serviceRegistry) + { + RegisterServices(serviceRegistry); + RegisterInputSystem(serviceRegistry); + RegisterMovementSystem(serviceRegistry); + RegisterCreatePlayerEntitySystem(serviceRegistry); + RegisterAnimatedMovementSystem(serviceRegistry); + + serviceRegistry.Register(); + } + + private static void RegisterServices(IServiceRegistry serviceRegistry) { serviceRegistry.Register(new PerContainerLifetime()); serviceRegistry.Register(new PerContainerLifetime()); + } + private static void RegisterInputSystem(IServiceRegistry serviceRegistry) + { serviceRegistry.Register(factory => { Contexts contexts = factory.GetInstance(); @@ -35,7 +49,10 @@ public void Compose(IServiceRegistry serviceRegistry) return new InputSystem(inputScanner, inputMovableGroup, logger); }, new PerContainerLifetime()); + } + private static void RegisterMovementSystem(IServiceRegistry serviceRegistry) + { serviceRegistry.Register(factory => { Contexts contexts = factory.GetInstance(); @@ -48,37 +65,39 @@ public void Compose(IServiceRegistry serviceRegistry) return new MovementSystem(movement, movableGroup, logger); }, new PerContainerLifetime()); + } + private static void RegisterCreatePlayerEntitySystem(IServiceRegistry serviceRegistry) + { serviceRegistry.Register(factory => { GraphicsDevice graphicsDevice = factory.GetInstance().GraphicsDevice; SpriteSheet spriteSheet = AnimatedCharactersFactory.LoadSpriteSheet(graphicsDevice, "Content/SpriteSheets/Player.aseprite"); - var idleDirAnimations = AnimatedCharactersFactory.CreateAnimations(spriteSheet, "Idle"); - var walkingDirAnimations = AnimatedCharactersFactory.CreateAnimations(spriteSheet, "Walking"); + var idleAnimations = AnimatedCharactersFactory.CreateAnimations(spriteSheet, "Idle"); + var walkingAnimations = AnimatedCharactersFactory.CreateAnimations(spriteSheet, "Walking"); - return new AnimatedMovementComponent( - new StateMachine(PlayerState.Idle), - idleDirAnimations, - walkingDirAnimations); + return new MovementAnimationComponent(idleAnimations, walkingAnimations); }); - serviceRegistry.Register(factory => new CreatePlayerEntitySystem(factory.GetInstance(), - factory.GetInstance())); + serviceRegistry.Register(); + serviceRegistry.Register(new PerContainerLifetime()); + } + + private static void RegisterAnimatedMovementSystem(IServiceRegistry serviceRegistry) + { serviceRegistry.Register(factory => { Contexts contexts = factory.GetInstance(); IAllOfMatcher animatedMovableMatcher = GameMatcher.AllOf(GameMatcher.Movable, - GameMatcher.AnimatedMovement); + GameMatcher.MovementAnimation); IGroup animatedMovableGroup = contexts.game.GetGroup(animatedMovableMatcher); var logger = factory.GetInstance(); return new AnimatedMovementSystem(animatedMovableGroup, logger); - }); - - serviceRegistry.Register(); + }, new PerContainerLifetime()); } } diff --git a/src/Libs/Components/GeneratedExtended/GameEntity.cs b/src/Libs/Components/GeneratedExtended/GameEntity.cs new file mode 100644 index 0000000..5703ed5 --- /dev/null +++ b/src/Libs/Components/GeneratedExtended/GameEntity.cs @@ -0,0 +1,16 @@ +using Components; +using Components.World; + +public partial class GameEntity +{ + // TODO: The kind of code generator? + public void AddMovementAnimation(MovementAnimationComponent component) => + AddMovementAnimation(newFacingDirection: component.FacingDirection, + newPlayingAnimation: component.PlayingAnimation, + newIdleAnimations: component.IdleAnimations, + newWalkingAnimations: component.WalkingAnimations, + newHasStopped: component.HasStopped); + + public void AddTransform(TransformComponent component) => + AddTransform(newPosition: component.Position, newVelocity: component.Velocity); +} diff --git a/src/Libs/Components/Jenny.properties b/src/Libs/Components/Jenny.properties index 90b7bf4..e4de1a2 100644 --- a/src/Libs/Components/Jenny.properties +++ b/src/Libs/Components/Jenny.properties @@ -43,7 +43,6 @@ Jenny.PostProcessors = Jenny.Plugins.AddFileHeaderPostProcessor, \ Jenny.Server.Port = 3333 Jenny.Client.Host = localhost Jenny.Plugins.ProjectPath = ../Systems/Systems.csproj -Jenny.Plugins.ProjectPath = ../Entities/Entities.csproj Jenny.Plugins.ProjectPath = Components.csproj Entitas.CodeGeneration.Plugins.Contexts = Game, \ Input, \ diff --git a/src/Libs/Components/MovementAnimationComponent.cs b/src/Libs/Components/MovementAnimationComponent.cs new file mode 100644 index 0000000..a2e07e7 --- /dev/null +++ b/src/Libs/Components/MovementAnimationComponent.cs @@ -0,0 +1,31 @@ +using Entitas; +using Microsoft.Xna.Framework; +using MonoGame.Aseprite.Sprites; +using Services.Math; + +namespace Components; + +public class MovementAnimationComponent : IComponent +{ + private const Direction DefaultFacing = Direction.Down; + + public AnimatedSprite PlayingAnimation; + public Dictionary IdleAnimations; + public Dictionary WalkingAnimations; + + public Vector2 FacingDirection = Vector2.UnitY; + public bool HasStopped; + + public MovementAnimationComponent() + { + } + + public MovementAnimationComponent(Dictionary idleAnimations, + Dictionary walkingAnimations) + { + IdleAnimations = idleAnimations; + WalkingAnimations = walkingAnimations; + + PlayingAnimation = idleAnimations[DefaultFacing]; + } +} diff --git a/src/Libs/Components/Sprites/AnimatedMovementComponent.cs b/src/Libs/Components/Sprites/AnimatedMovementComponent.cs deleted file mode 100644 index 3fd574f..0000000 --- a/src/Libs/Components/Sprites/AnimatedMovementComponent.cs +++ /dev/null @@ -1,91 +0,0 @@ -using Entitas; -using Microsoft.Xna.Framework; -using MonoGame.Aseprite.Sprites; -using Services.Math; -using Stateless; - -namespace Components.Sprites; - -public class AnimatedMovementComponent : IComponent -{ - private const RadDir DefaultFacing = RadDir.Down; - - public StateMachine StateMachine; - - public StateMachine.TriggerWithParameters - MoveWithParameters; - - private readonly Dictionary _idleAnimations; - private readonly Dictionary _walkingAnimations; - - private Vector2 _lastDirection; - - public AnimatedSprite PlayingAnimation; - - public AnimatedMovementComponent(StateMachine stateMachine, - Dictionary idleAnimations, - Dictionary walkingAnimations) - { - StateMachine = stateMachine; - MoveWithParameters = StateMachine.SetTriggerParameters(PlayerTrigger.Move); - - _idleAnimations = idleAnimations; - _walkingAnimations = walkingAnimations; - PlayingAnimation = _idleAnimations[DefaultFacing]; - - ConfigureStateMachine(); - - StateMachine.Activate(); - } - - private void ConfigureStateMachine() - { - StateMachine - .Configure(PlayerState.Idle) - .OnEntry(StopFacingTrace) - .Ignore(PlayerTrigger.Stop) - .Permit(PlayerTrigger.Move, PlayerState.Moving); - - StateMachine - .Configure(PlayerState.Moving) - .OnEntryFrom(MoveWithParameters, MoveFacingTrace) - .PermitReentry(PlayerTrigger.Move) - .Permit(PlayerTrigger.Stop, PlayerState.Idle); - } - - private void StopFacingTrace() - { - RadDir radDir = MathUtils.Rad8DirYFlipped(_lastDirection); - PlayingAnimation = _idleAnimations[radDir]; - PlayingAnimation.SetFrame(0); - } - - private void MoveFacingTrace(Vector2 direction) - { - RadDir radDir = MathUtils.Rad8DirYFlipped(direction); - AnimatedSprite walkingAnimation = _walkingAnimations[radDir]; - - if (AreDifferentSprites(PlayingAnimation, walkingAnimation)) - { - PlayingAnimation = walkingAnimation; - } - - _lastDirection = direction; - } - - private static bool AreDifferentSprites(Sprite left, Sprite right) => - left.Name != right.Name || - left.FlipHorizontally != right.FlipHorizontally; -} - -public enum PlayerState -{ - Idle, - Moving, -} - -public enum PlayerTrigger -{ - Stop, - Move, -} diff --git a/src/Libs/Components/World/RectangleCollisionComponent.cs b/src/Libs/Components/World/RectangleCollisionComponent.cs index 8dffe6f..fa4e0cc 100644 --- a/src/Libs/Components/World/RectangleCollisionComponent.cs +++ b/src/Libs/Components/World/RectangleCollisionComponent.cs @@ -1,6 +1,5 @@ using Entitas; using Microsoft.Xna.Framework; -using Anchor = Services.Math.RadDir; namespace Components.World; diff --git a/src/Libs/External/Entitas.Extended/Entitas.Extended.csproj b/src/Libs/External/Entitas.Extended/Entitas.Extended.csproj index d8e83f5..d0663cd 100644 --- a/src/Libs/External/Entitas.Extended/Entitas.Extended.csproj +++ b/src/Libs/External/Entitas.Extended/Entitas.Extended.csproj @@ -11,4 +11,8 @@ + + + + diff --git a/src/Libs/Services/Factories/AnimatedCharactersFactory.cs b/src/Libs/Services/Factories/AnimatedCharactersFactory.cs index 6fd9ecb..3b769f5 100644 --- a/src/Libs/Services/Factories/AnimatedCharactersFactory.cs +++ b/src/Libs/Services/Factories/AnimatedCharactersFactory.cs @@ -8,8 +8,8 @@ namespace Services.Factories; public static class AnimatedCharactersFactory { - private static readonly IReadOnlyList Directions = - new[] { RadDir.Right, RadDir.Down, RadDir.Left, RadDir.Up }; + private static readonly IReadOnlyList Directions = + new[] { Direction.Right, Direction.Down, Direction.Left, Direction.Up }; public static SpriteSheet LoadSpriteSheet(GraphicsDevice graphicsDevice, string path) { @@ -17,15 +17,15 @@ public static SpriteSheet LoadSpriteSheet(GraphicsDevice graphicsDevice, string return SpriteSheetProcessor.Process(graphicsDevice, asepriteFile); } - private static string BuildTag(string action, RadDir dir) => $"{action}{dir.ToString()}"; + private static string BuildTag(string action, Direction dir) => $"{action}{dir.ToString()}"; - private static AnimatedSprite CreateAnimation(SpriteSheet spriteSheet, string action, RadDir direction) + private static AnimatedSprite CreateAnimation(SpriteSheet spriteSheet, string action, Direction direction) { AnimatedSprite animatedSprite; - if (direction == RadDir.Left) + if (direction == Direction.Left) { - string rightAnimationTag = BuildTag(action, RadDir.Right); + string rightAnimationTag = BuildTag(action, Direction.Right); animatedSprite = spriteSheet.CreateAnimatedSprite(rightAnimationTag); animatedSprite.FlipHorizontally = true; @@ -41,16 +41,16 @@ private static AnimatedSprite CreateAnimation(SpriteSheet spriteSheet, string ac return animatedSprite; } - public static Dictionary CreateAnimations(SpriteSheet spriteSheet, string action) + public static Dictionary CreateAnimations(SpriteSheet spriteSheet, string action) { - Dictionary dictionary = + Dictionary dictionary = Directions.ToDictionary(dir => dir, dir => CreateAnimation(spriteSheet, action, dir)); // Temp hack - dictionary.Add(RadDir.DownLeft, dictionary[RadDir.Left]); - dictionary.Add(RadDir.DownRight, dictionary[RadDir.Right]); - dictionary.Add(RadDir.UpLeft, dictionary[RadDir.Left]); - dictionary.Add(RadDir.UpRight, dictionary[RadDir.Right]); + dictionary.Add(Direction.DownLeft, dictionary[Direction.Left]); + dictionary.Add(Direction.DownRight, dictionary[Direction.Right]); + dictionary.Add(Direction.UpLeft, dictionary[Direction.Left]); + dictionary.Add(Direction.UpRight, dictionary[Direction.Right]); return dictionary; } diff --git a/src/Libs/Services/Math/MathUtils.cs b/src/Libs/Services/Math/MathUtils.cs index 5ef54df..3038159 100644 --- a/src/Libs/Services/Math/MathUtils.cs +++ b/src/Libs/Services/Math/MathUtils.cs @@ -2,7 +2,7 @@ namespace Services.Math; -public enum RadDir +public enum Direction { Right, UpRight, @@ -27,13 +27,13 @@ private static double RadDir(float x, float y, int sectors) return System.Math.Floor(radians / (2 * System.Math.PI) * sectors); } - public static RadDir Rad8Dir(Vector2 dir) => (RadDir)RadDir(dir.X, dir.Y, sectors: 8); + public static Direction Rad8Dir(Vector2 dir) => (Direction)RadDir(dir.X, dir.Y, sectors: 8); // Useful as MonoGame has Y-flipped coordinate system - public static RadDir Rad8DirYFlipped(Vector2 dir) => (RadDir)RadDir(dir.X, -dir.Y, sectors: 8); + public static Direction Rad8DirYFlipped(Vector2 dir) => (Direction)RadDir(dir.X, -dir.Y, sectors: 8); - public static RadDir Rad4Dir(Vector2 dir) => (RadDir)RadDir(dir.X, dir.Y, sectors: 4); + public static Direction Rad4Dir(Vector2 dir) => (Direction)RadDir(dir.X, dir.Y, sectors: 4); // Useful as MonoGame has Y-flipped coordinate system - public static RadDir Rad4DirYFlipped(Vector2 dir) => (RadDir)RadDir(dir.X, dir.Y, sectors: 4); + public static Direction Rad4DirYFlipped(Vector2 dir) => (Direction)RadDir(dir.X, dir.Y, sectors: 4); } diff --git a/src/Libs/Systems/AnimatedMovementSystem.cs b/src/Libs/Systems/AnimatedMovementSystem.cs index 657972e..c8b4c0c 100644 --- a/src/Libs/Systems/AnimatedMovementSystem.cs +++ b/src/Libs/Systems/AnimatedMovementSystem.cs @@ -1,9 +1,11 @@ -using Components.Sprites; +using Components; using Entitas; using Entitas.Extended; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; +using MonoGame.Aseprite.Sprites; using Serilog; +using Services.Math; using IExecuteSystem = Entitas.Extended.IExecuteSystem; namespace Systems; @@ -21,23 +23,22 @@ public AnimatedMovementSystem(IGroup group, ILogger logger) public void Execute(GameTime gameTime) { - GameEntity[]? entities = _group.GetEntities(); + GameEntity[] entities = _group.GetEntities(); foreach (GameEntity e in entities) { Vector2 velocity = e.transform.Velocity; - AnimatedMovementComponent movementAnimatedSprites = e.animatedMovement; - // TODO: refactor and put it in service system + // TODO: refactor and put it in a service system? if (velocity.Equals(Vector2.Zero)) { - movementAnimatedSprites.StateMachine.Fire(PlayerTrigger.Stop); + StopFacingTrace(e.movementAnimation); } else { - movementAnimatedSprites.StateMachine.Fire(movementAnimatedSprites.MoveWithParameters, velocity); + MoveFacingTrace(e.movementAnimation, velocity); } - movementAnimatedSprites.PlayingAnimation.Update(gameTime); + e.movementAnimation.PlayingAnimation.Update(gameTime); } } @@ -49,11 +50,42 @@ public void Draw(GameTime gameTime, SpriteBatch spriteBatch) foreach (GameEntity e in entities) { Vector2 position = e.transform.Position; - AnimatedMovementComponent movementAnimatedSprites = e.animatedMovement; - movementAnimatedSprites.PlayingAnimation.Draw(spriteBatch, position); + e.movementAnimation.PlayingAnimation.Draw(spriteBatch, position); } spriteBatch.End(); } + + private static void StopFacingTrace(MovementAnimationComponent component) + { + if (component.HasStopped) + { + return; + } + + Direction direction = MathUtils.Rad8DirYFlipped(component.FacingDirection); + + component.PlayingAnimation = component.IdleAnimations[direction]; + component.PlayingAnimation.SetFrame(0); + component.HasStopped = true; + } + + private static void MoveFacingTrace(MovementAnimationComponent component, Vector2 velocity) + { + Direction direction = MathUtils.Rad8DirYFlipped(velocity); + AnimatedSprite walkingAnimation = component.WalkingAnimations[direction]; + + if (AreDifferentSprites(component.PlayingAnimation, walkingAnimation)) + { + component.PlayingAnimation = walkingAnimation; + } + + component.FacingDirection = velocity; + component.HasStopped = false; + } + + private static bool AreDifferentSprites(Sprite left, Sprite right) => + left.Name != right.Name || + left.FlipHorizontally != right.FlipHorizontally; } diff --git a/src/Libs/Systems/CreatePlayerEntitySystem.cs b/src/Libs/Systems/CreatePlayerEntitySystem.cs index 4ed3e3f..aadd603 100644 --- a/src/Libs/Systems/CreatePlayerEntitySystem.cs +++ b/src/Libs/Systems/CreatePlayerEntitySystem.cs @@ -1,36 +1,31 @@ -using Components.Sprites; +using Components; using Components.World; using Entitas; -using Microsoft.Xna.Framework; namespace Systems; -public class CreatePlayerEntitySystem : IInitializeSystem +public class CreatePlayerEntitySystem : ISystem { private readonly Contexts _contexts; - private readonly AnimatedMovementComponent _animatedMovementComponent; - private int _componentsCount = 0; - public CreatePlayerEntitySystem(Contexts contexts, AnimatedMovementComponent animatedMovementComponent) + public CreatePlayerEntitySystem(Contexts contexts, + MovementAnimationComponent movementAnimationComponent, + TransformComponent transformComponent) { _contexts = contexts; - _animatedMovementComponent = animatedMovementComponent; + CreateEntity(movementAnimationComponent, transformComponent); } - public void Initialize() + private void CreateEntity(MovementAnimationComponent movementAnimationComponent, + TransformComponent transformComponent) { GameEntity e = _contexts.game.CreateEntity(); - e.AddComponent(_componentsCount++, _animatedMovementComponent); - // e.AddComponent(_componentsCount++, new PlayerComponent()); - // e.AddComponent(_componentsCount++, new MovableComponent()); - // e.AddComponent(_componentsCount++, new TransformComponent()); - // e.AddComponent(_componentsCount++, new RectangleCollisionComponent()); - e.isPlayer = true; e.isMovable = true; + e.AddMovementAnimation(movementAnimationComponent); + e.AddTransform(transformComponent); - e.AddTransform(new Vector2(), new Vector2()); - e.AddRectangleCollision(new Rectangle(0, 0, 16, 16)); + // e.AddRectangleCollision(new Rectangle(0, 0, 16, 16)); } } diff --git a/src/UnitTests/UnitTests.Services/Rad8Dir.cs b/src/UnitTests/UnitTests.Services/Rad8Dir.cs index 28a2514..bb100d5 100644 --- a/src/UnitTests/UnitTests.Services/Rad8Dir.cs +++ b/src/UnitTests/UnitTests.Services/Rad8Dir.cs @@ -15,48 +15,48 @@ public void Setup() public void Right() { Vector2 right = new(1, 0); - RadDir radDir = MathUtils.Rad8Dir(right); - Assert.That(radDir, Is.EqualTo(RadDir.Right)); + Direction direction = MathUtils.Rad8Dir(right); + Assert.That(direction, Is.EqualTo(Direction.Right)); } [Test] public void UpRight() { Vector2 upRight = new(1, 1); - RadDir radDir = MathUtils.Rad8Dir(upRight); - Assert.That(radDir, Is.EqualTo(RadDir.UpRight)); + Direction direction = MathUtils.Rad8Dir(upRight); + Assert.That(direction, Is.EqualTo(Direction.UpRight)); } [Test] public void Up() { Vector2 up = new(0, 1); - RadDir radDir = MathUtils.Rad8Dir(up); - Assert.That(radDir, Is.EqualTo(RadDir.Up)); + Direction direction = MathUtils.Rad8Dir(up); + Assert.That(direction, Is.EqualTo(Direction.Up)); } [Test] public void UpLeft() { Vector2 upLeft = new(-1, 1); - RadDir radDir = MathUtils.Rad8Dir(upLeft); - Assert.That(radDir, Is.EqualTo(RadDir.UpLeft)); + Direction direction = MathUtils.Rad8Dir(upLeft); + Assert.That(direction, Is.EqualTo(Direction.UpLeft)); } [Test] public void Left() { Vector2 left = new(-1, 0); - RadDir radDir = MathUtils.Rad8Dir(left); - Assert.That(radDir, Is.EqualTo(RadDir.Left)); + Direction direction = MathUtils.Rad8Dir(left); + Assert.That(direction, Is.EqualTo(Direction.Left)); } [Test] public void DownLeft() { Vector2 downLeft = new(-1, -1); - RadDir radDir = MathUtils.Rad8Dir(downLeft); - Assert.That(radDir, Is.EqualTo(RadDir.DownLeft)); + Direction direction = MathUtils.Rad8Dir(downLeft); + Assert.That(direction, Is.EqualTo(Direction.DownLeft)); } @@ -64,15 +64,15 @@ public void DownLeft() public void Down() { Vector2 down = new(0, -1); - RadDir radDir = MathUtils.Rad8Dir(down); - Assert.That(radDir, Is.EqualTo(RadDir.Down)); + Direction direction = MathUtils.Rad8Dir(down); + Assert.That(direction, Is.EqualTo(Direction.Down)); } [Test] public void DownRight() { Vector2 downRight = new(1, -1); - RadDir radDir = MathUtils.Rad8Dir(downRight); - Assert.That(radDir, Is.EqualTo(RadDir.DownRight)); + Direction direction = MathUtils.Rad8Dir(downRight); + Assert.That(direction, Is.EqualTo(Direction.DownRight)); } }