diff --git a/src/Apps/GameDesktop/CompositionRoots/Components/ComponentsCompositionRoot.cs b/src/Apps/GameDesktop/CompositionRoots/Components/ComponentsCompositionRoot.cs index fc1aaa2..742fbe2 100644 --- a/src/Apps/GameDesktop/CompositionRoots/Components/ComponentsCompositionRoot.cs +++ b/src/Apps/GameDesktop/CompositionRoots/Components/ComponentsCompositionRoot.cs @@ -35,6 +35,7 @@ private static void RegisterDataComponents(IServiceRegistry serviceRegistry) private static void RegisterTagComponents(IServiceRegistry serviceRegistry) { + RegisterCameraComponent(serviceRegistry); RegisterPlayerMovementComponent(serviceRegistry); } @@ -49,6 +50,11 @@ private static void RegisterAnimatedRenderComponents(IServiceRegistry serviceReg RegisterCharacterAnimatorComponent(serviceRegistry); } + private static void RegisterCameraComponent(IServiceRegistry serviceRegistry) + { + serviceRegistry.RegisterSingleton(_ => new CameraComponent()); + } + private static void RegisterPlayerMovementComponent(IServiceRegistry serviceRegistry) { serviceRegistry.RegisterSingleton(_ => new InputMovableComponent()); diff --git a/src/Apps/GameDesktop/CompositionRoots/Entities/PlayerEntityCompositionRoot.cs b/src/Apps/GameDesktop/CompositionRoots/Entities/PlayerEntityCompositionRoot.cs index 4a04277..72fd45c 100644 --- a/src/Apps/GameDesktop/CompositionRoots/Entities/PlayerEntityCompositionRoot.cs +++ b/src/Apps/GameDesktop/CompositionRoots/Entities/PlayerEntityCompositionRoot.cs @@ -18,6 +18,7 @@ private static void RegisterEntity(IServiceRegistry serviceRegistry) => factory.GetInstance(), factory.GetInstance(), factory.GetInstance("PlayerEntity"), + factory.GetInstance(), factory.GetInstance(), factory.GetInstance(), factory.GetInstance("PlayerEntity"))); diff --git a/src/Apps/GameDesktop/CompositionRoots/GameCompositionRoot.cs b/src/Apps/GameDesktop/CompositionRoots/GameCompositionRoot.cs index 384c014..8cbf22d 100644 --- a/src/Apps/GameDesktop/CompositionRoots/GameCompositionRoot.cs +++ b/src/Apps/GameDesktop/CompositionRoots/GameCompositionRoot.cs @@ -1,7 +1,6 @@ using GameDesktop.Resources.Internal; using LightInject; using Microsoft.Xna.Framework; -using MonoGame.ImGuiNet; using Serilog; namespace GameDesktop.CompositionRoots; @@ -18,13 +17,14 @@ public void Compose(IServiceRegistry serviceRegistry) Game game = new(factory.GetInstance(), factory.GetInstance()) { - IsMouseVisible = IsMouseVisible, Content = { RootDirectory = AppVariable.ContentRootDirectory, } + IsMouseVisible = IsMouseVisible, + Content = { RootDirectory = AppVariable.ContentRootDirectory, }, }; // Hack. Resolving cycle dependency issue (fundamental architecture) // Implicitly adds itself in the game services container. new GraphicsDeviceManager(game); - + return game; }); } diff --git a/src/Apps/GameDesktop/Game.cs b/src/Apps/GameDesktop/Game.cs index 52ca732..cf33859 100644 --- a/src/Apps/GameDesktop/Game.cs +++ b/src/Apps/GameDesktop/Game.cs @@ -28,6 +28,7 @@ public class Game : Microsoft.Xna.Framework.Game private SpriteBatch _spriteBatch; private SystemsGroup _systemsGroup; + private SystemsGroup _preRenderSystemsGroup; private SystemsGroup _renderSystemsGroup; private SystemsGroup _debugSystemsGroup; @@ -84,8 +85,12 @@ protected override void LoadContent() _systemsGroup.AddSystem(new InputSystem(world, new KeyboardInput())); _systemsGroup.AddSystem(new MovementSystem(world, new SimpleMovement())); + _preRenderSystemsGroup = world.CreateSystemsGroup(); + _preRenderSystemsGroup.AddSystem(new RenderCharacterMovementSystem(world, _spriteBatch)); + _renderSystemsGroup = world.CreateSystemsGroup(); - _renderSystemsGroup.AddSystem(new RenderCharacterMovementSystem(world, _spriteBatch)); + _renderSystemsGroup.AddSystem(new CameraSystem(world, + new FollowingCamera(_spriteBatch, new Viewport(0, 0, 800, 480)))); #if DEBUG _debugSystemsGroup = world.CreateSystemsGroup(); @@ -129,6 +134,8 @@ protected override void Draw(GameTime gameTime) GraphicsDevice.Clear(Color.CornflowerBlue); _spriteBatch.Begin(samplerState: SamplerState.PointClamp); + // I guess, pre-render, pre-ui systems would go in update, to avoid frames skipping + _preRenderSystemsGroup.Update(deltaTime); _renderSystemsGroup.Update(deltaTime); _spriteBatch.End(); diff --git a/src/Libs/Components/Tags/CameraComponent.cs b/src/Libs/Components/Tags/CameraComponent.cs new file mode 100644 index 0000000..fa352da --- /dev/null +++ b/src/Libs/Components/Tags/CameraComponent.cs @@ -0,0 +1,7 @@ +using Scellecs.Morpeh; + +namespace Components.Tags; + +public struct CameraComponent : IComponent +{ +} diff --git a/src/Libs/Entities/PlayerEntity.cs b/src/Libs/Entities/PlayerEntity.cs index 19df84d..87644e4 100644 --- a/src/Libs/Entities/PlayerEntity.cs +++ b/src/Libs/Entities/PlayerEntity.cs @@ -41,6 +41,7 @@ public class PlayerEntity private readonly InputMovableComponent _inputMovable; private readonly MovableComponent _movable; private readonly TransformComponent _transform; + private readonly CameraComponent _cameraComponent; private readonly RectangleCollisionComponent _rectangleCollision; private readonly MovementAnimationsComponent _movementAnimations; private readonly CharacterAnimatorComponent _characterAnimator; @@ -48,6 +49,7 @@ public class PlayerEntity public PlayerEntity(InputMovableComponent inputMovable, MovableComponent movable, TransformComponent transform, + CameraComponent cameraComponent, RectangleCollisionComponent rectangleCollision, MovementAnimationsComponent movementAnimations, CharacterAnimatorComponent characterAnimator) @@ -55,6 +57,7 @@ public PlayerEntity(InputMovableComponent inputMovable, _inputMovable = inputMovable; _movable = movable; _transform = transform; + _cameraComponent = cameraComponent; _rectangleCollision = rectangleCollision; _movementAnimations = movementAnimations; _characterAnimator = characterAnimator; @@ -73,6 +76,7 @@ public Entity Create(World @in) private void AddTags(Entity e) { + e.AddComponent(_cameraComponent); e.AddComponent(_inputMovable); e.AddComponent(_movable); } diff --git a/src/Libs/Systems/Render/CameraFollowingSystem.cs b/src/Libs/Systems/Render/CameraFollowingSystem.cs deleted file mode 100644 index 1ad1c59..0000000 --- a/src/Libs/Systems/Render/CameraFollowingSystem.cs +++ /dev/null @@ -1,49 +0,0 @@ -// using Entitas; -// using Entitas.Extended; -// using Microsoft.Xna.Framework; -// using Microsoft.Xna.Framework.Graphics; -// -// namespace Systems; -// -// public class CameraFollowingSystem : IDrawSystem -// { -// private readonly Contexts _contexts; -// private readonly IGroup _group; -// -// public CameraFollowingSystem(Contexts contexts, IGroup group) -// { -// _contexts = contexts; -// _group = group; -// } -// -// // todo: refactor, put the logic in impl -// // todo: smooth diagonal movement -// private Vector2 GetPosition(GameEntity target) => -// new( -// (float)target.camera.Size.Width / 2 - -// (float)target.movementAnimation.PlayingAnimation.Width / 2, -// (float)target.camera.Size.Height / 2 - -// (float)target.movementAnimation.PlayingAnimation.Height / 2); -// -// public void Draw(GameTime gameTime, SpriteBatch spriteBatch) -// { -// GameEntity[] entities = _group.GetEntities(); -// -// spriteBatch.Begin(samplerState: SamplerState.PointWrap); -// -// var target = _contexts.game.cameraEntity; -// foreach (GameEntity e in entities) -// { -// Vector2 otherAt = e.transform.Position - (target?.transform.Position ?? Vector2.Zero); -// if (e.hasSprite) -// { -// e.sprite.Sprite.Draw(spriteBatch, otherAt); -// // todo: drawing complex entities' sprite/animated components -// } -// -// target?.movementAnimation.PlayingAnimation.Draw(spriteBatch, GetPosition(target)); -// } -// -// spriteBatch.End(); -// } -// } diff --git a/src/Libs/Systems/Render/CameraSystem.cs b/src/Libs/Systems/Render/CameraSystem.cs new file mode 100644 index 0000000..31e25ad --- /dev/null +++ b/src/Libs/Systems/Render/CameraSystem.cs @@ -0,0 +1,201 @@ +using System.Numerics; +using Components.Data; +using Components.Render.Animation; +using Components.Render.Static; +using Components.Tags; +using Microsoft.Xna.Framework.Graphics; +using Scellecs.Morpeh; + +namespace Systems.Render; + +public interface ICamera +{ + void Render(Entity e); +} + +public abstract class BaseCamera +{ + private readonly SpriteBatch _spriteBatch; + protected readonly Viewport _viewport; + + protected BaseCamera(SpriteBatch spriteBatch, Viewport viewport) + { + _spriteBatch = spriteBatch; + _viewport = viewport; + } + + protected void RenderCharacterAnimator(Entity e, Vector2 at) + { + if (!e.Has()) + { + return; + } + + ref var animator = ref e.GetComponent(); + animator.Animation.Draw(_spriteBatch, at); + } + + protected void RenderSprite(Entity e, Vector2 at) + { + if (!e.Has()) + { + return; + } + + ref var spriteComponent = ref e.GetComponent(); + spriteComponent.Sprite.Draw(_spriteBatch, at); + } +} + +public class StaticCamera : BaseCamera, ICamera +{ + public StaticCamera(SpriteBatch spriteBatch, Viewport viewport) : base(spriteBatch, viewport) + { + } + + public void Render(Entity e) + { + if (!e.Has()) + { + return; + } + + ref var transform = ref e.GetComponent(); + + base.RenderCharacterAnimator(e, at: transform.Position); + base.RenderSprite(e, at: transform.Position); + } +} + +public class FollowingCamera : BaseCamera, ICamera +{ + private Vector2 _position; + + public FollowingCamera(SpriteBatch spriteBatch, Viewport viewport) : base(spriteBatch, viewport) + { + } + + public void Render(Entity e) + { + if (!e.Has()) + { + return; + } + + ref var transform = ref e.GetComponent(); + Vector2 position = transform.Position; + + if (e.Has()) + { + _position = GetCenteredPosition(off: position); + } + + Vector2 relativePosition = position - _position; + + base.RenderCharacterAnimator(e, at: relativePosition); + base.RenderSprite(e, at: relativePosition); + } + + private Vector2 GetCenteredPosition(Vector2 off) => new( + off.X - _viewport.Width / 2, + off.Y - _viewport.Height / 2); +} + +public class CameraSystem : ISystem +{ + private readonly ICamera _camera; + public World World { get; set; } + + public CameraSystem(World world, ICamera camera) + { + World = world; + _camera = camera; + } + + public void OnAwake() + { + } + + public void OnUpdate(float deltaTime) + { + // TODO: Components in the range of visibility (world grid system?) + Filter filter = World.Filter.With().Build(); + + IEnumerable entities = SortEntitiesByYPosition(filter); + + foreach (Entity e in entities) + { + _camera.Render(e); + } + } + + private static IEnumerable SortEntitiesByYPosition(Filter filter) + { + List entities = new List(); + + foreach (Entity e in filter) + { + entities.Add(e); + } + + return entities.OrderBy(x => + { + ref var transform = ref x.GetComponent(); + return transform.Position.Y; + }); + } + + public void Dispose() + { + } +} + +// using Entitas; +// using Entitas.Extended; +// using Microsoft.Xna.Framework; +// using Microsoft.Xna.Framework.Graphics; +// +// namespace Systems; +// +// public class CameraFollowingSystem : IDrawSystem +// { +// private readonly Contexts _contexts; +// private readonly IGroup _group; +// +// public CameraFollowingSystem(Contexts contexts, IGroup group) +// { +// _contexts = contexts; +// _group = group; +// } +// +// // todo: refactor, put the logic in impl +// // todo: smooth diagonal movement +// private Vector2 GetPosition(GameEntity target) => +// new( +// (float)target.camera.Size.Width / 2 - +// (float)target.movementAnimation.PlayingAnimation.Width / 2, +// (float)target.camera.Size.Height / 2 - +// (float)target.movementAnimation.PlayingAnimation.Height / 2); +// +// public void Draw(GameTime gameTime, SpriteBatch spriteBatch) +// { +// GameEntity[] entities = _group.GetEntities(); +// +// spriteBatch.Begin(samplerState: SamplerState.PointWrap); +// +// var target = _contexts.game.cameraEntity; +// foreach (GameEntity e in entities) +// { +// Vector2 otherAt = e.transform.Position - (target?.transform.Position ?? Vector2.Zero); +// if (e.hasSprite) +// { +// e.sprite.Sprite.Draw(spriteBatch, otherAt); +// // todo: drawing complex entities' sprite/animated components +// } +// +// target?.movementAnimation.PlayingAnimation.Draw(spriteBatch, GetPosition(target)); +// } +// +// spriteBatch.End(); +// } +// } diff --git a/src/Libs/Systems/Render/RenderCharacterMovementSystem.cs b/src/Libs/Systems/Render/RenderCharacterMovementSystem.cs index 379aab0..0cc0d5a 100644 --- a/src/Libs/Systems/Render/RenderCharacterMovementSystem.cs +++ b/src/Libs/Systems/Render/RenderCharacterMovementSystem.cs @@ -15,6 +15,7 @@ public class RenderCharacterMovementSystem : ISystem public RenderCharacterMovementSystem(World world, SpriteBatch spriteBatch) { + World = world; _spriteBatch = spriteBatch; } @@ -35,29 +36,38 @@ public void OnUpdate(float deltaTime) ref var animator = ref e.GetComponent(); ref var transform = ref e.GetComponent(); - animator.Animation.Draw(_spriteBatch, transform.Position); + // 2. And this one could be in the draw state + // animator.Animation.Draw(_spriteBatch, transform.Position); + + // 1. Actually, all of this could be put in the pre-draw animator.Animation.Update(deltaTime); - AnimatedSprite animation; + AnimatedSprite animation = GetAnimation(transform, animations, animator); + animator.Facing = GetDirection(transform, animator); - if (transform.Velocity.Equals(Vector2.Zero)) - { - animation = animations.IdleAnimations[animator.Facing]; - } - else + if (animator.Animation == animation) { - animator.Facing = MathUtils.Rad8DirYFlipped(transform.Velocity); - animation = animations.WalkingAnimations[animator.Facing]; + continue; } - if (animator.Animation != animation) - { - animator.Animation = animation; - animator.Animation.Play(); - } + animator.Animation = animation; + // .Play() is called in the AnimatedCharactersFactory (on the step of creation), + // otherwise, you'd call it manually here. } } + private static AnimatedSprite GetAnimation(TransformComponent transform, + MovementAnimationsComponent animations, + CharacterAnimatorComponent animator) => + transform.Velocity.Equals(Vector2.Zero) + ? animations.IdleAnimations[animator.Facing] + : animations.WalkingAnimations[animator.Facing]; + + private static Direction GetDirection(TransformComponent transform, CharacterAnimatorComponent animator) => + transform.Velocity.Equals(Vector2.Zero) + ? animator.Facing + : MathUtils.Rad8DirYFlipped(transform.Velocity); + public void Dispose() { } diff --git a/src/UnitTests/UnitTests.Entities/PlayerEntity.cs b/src/UnitTests/UnitTests.Entities/PlayerEntity.cs index 33f61d1..24333e9 100644 --- a/src/UnitTests/UnitTests.Entities/PlayerEntity.cs +++ b/src/UnitTests/UnitTests.Entities/PlayerEntity.cs @@ -30,6 +30,7 @@ public void PlayerEntity_IsCreatedInTheWorld() Entity playerEntity = new PlayerEntity(new InputMovableComponent(), new MovableComponent(), new TransformComponent(), + new CameraComponent(), new RectangleCollisionComponent(), new MovementAnimationsComponent(), new CharacterAnimatorComponent()) @@ -48,17 +49,19 @@ public void PlayerEntity_HasComponents() Entity playerEntity = new PlayerEntity(new InputMovableComponent(), new MovableComponent(), new TransformComponent(), + new CameraComponent(), new RectangleCollisionComponent(), new MovementAnimationsComponent(), new CharacterAnimatorComponent()) .Create(@in: _world); - { _world.TryGetEntity(playerEntity.ID, out Entity result); - Assert.That(result.Has(), Is.True); - Assert.That(result.Has(), Is.True); - // ... + Assert.Multiple(() => + { + Assert.That(result.Has(), Is.True); + Assert.That(result.Has(), Is.True); + }); } } }