From 5363746a18bc68acf638ff714d53c23bd43416f0 Mon Sep 17 00:00:00 2001 From: Ethan Moffat Date: Thu, 4 May 2023 21:49:10 -0700 Subject: [PATCH] Make animation timings wrok more consistently. Fixes: - Main character moving/attacking more slowly than vanilla client - Other characters moving more slowly than vanilla client, causing weird walk glitches when their walk ended - Main character timestamps not having high enough resolution - Main character timestamps not being sent at consistent enough intervals --- EOLib/Domain/Character/CharacterActions.cs | 17 +++++++------ EOLib/GameStartTimeRepository.cs | 25 +++++++++++++++++++ EOLib/misc.cs | 3 ++- EndlessClient/GameExecution/EndlessGame.cs | 16 ++++++------ .../Rendering/Character/CharacterAnimator.cs | 13 +++++----- .../Rendering/FixedTimeStepRepository.cs | 4 +-- 6 files changed, 54 insertions(+), 24 deletions(-) create mode 100644 EOLib/GameStartTimeRepository.cs diff --git a/EOLib/Domain/Character/CharacterActions.cs b/EOLib/Domain/Character/CharacterActions.cs index 9ee0de5df..5e2106f04 100644 --- a/EOLib/Domain/Character/CharacterActions.cs +++ b/EOLib/Domain/Character/CharacterActions.cs @@ -16,14 +16,17 @@ public class CharacterActions : ICharacterActions private readonly IPacketSendService _packetSendService; private readonly ICharacterRepository _characterRepository; private readonly IESFFileProvider _spellFileProvider; + private readonly IGameStartTimeProvider _gameStartTimeProvider; public CharacterActions(IPacketSendService packetSendService, ICharacterRepository characterRepository, - IESFFileProvider spellFileProvider) + IESFFileProvider spellFileProvider, + IGameStartTimeProvider gameStartTimeProvider) { _packetSendService = packetSendService; _characterRepository = characterRepository; _spellFileProvider = spellFileProvider; + _gameStartTimeProvider = gameStartTimeProvider; } public void Face(EODirection direction) @@ -43,7 +46,7 @@ public void Walk() var packet = new PacketBuilder(PacketFamily.Walk, admin ? PacketAction.Admin : PacketAction.Player) .AddChar((int)renderProperties.Direction) - .AddThree(DateTime.Now.ToEOTimeStamp()) + .AddThree(_gameStartTimeProvider.TimeStamp) .AddChar(renderProperties.GetDestinationX()) .AddChar(renderProperties.GetDestinationY()) .Build(); @@ -59,7 +62,7 @@ public void Attack() var packet = new PacketBuilder(PacketFamily.Attack, PacketAction.Use) .AddChar((int) _characterRepository.MainCharacter.RenderProperties.Direction) - .AddThree(DateTime.Now.ToEOTimeStamp()) + .AddThree(_gameStartTimeProvider.TimeStamp) .Build(); _packetSendService.SendPacket(packet); @@ -100,7 +103,7 @@ public void PrepareCastSpell(int spellId) { var packet = new PacketBuilder(PacketFamily.Spell, PacketAction.Request) .AddShort(spellId) - .AddThree(DateTime.Now.ToEOTimeStamp()) + .AddThree(_gameStartTimeProvider.TimeStamp) .Build(); _packetSendService.SendPacket(packet); @@ -128,7 +131,7 @@ public void CastSpell(int spellId, ISpellTargetable target) { builder = builder .AddShort(spellId) - .AddThree(DateTime.Now.ToEOTimeStamp()); + .AddThree(_gameStartTimeProvider.TimeStamp); } else { @@ -146,13 +149,13 @@ public void CastSpell(int spellId, ISpellTargetable target) .AddShort(1) // unknown .AddShort(spellId) .AddShort(target.Index) - .AddThree(DateTime.Now.ToEOTimeStamp()); + .AddThree(_gameStartTimeProvider.TimeStamp); } else { builder = builder .AddShort(spellId) - .AddInt(DateTime.Now.ToEOTimeStamp()); + .AddInt(_gameStartTimeProvider.TimeStamp); } } diff --git a/EOLib/GameStartTimeRepository.cs b/EOLib/GameStartTimeRepository.cs new file mode 100644 index 000000000..38c656c23 --- /dev/null +++ b/EOLib/GameStartTimeRepository.cs @@ -0,0 +1,25 @@ +using AutomaticTypeMapper; +using System; +using System.Diagnostics; + +namespace EOLib +{ + public interface IGameStartTimeProvider + { + DateTime StartTime { get; } + + Stopwatch Elapsed { get; } + + int TimeStamp { get; } + } + + [AutoMappedType(IsSingleton = true)] + public class GameStartTimeRepository : IGameStartTimeProvider + { + public DateTime StartTime { get; } = DateTime.UtcNow; + + public Stopwatch Elapsed { get; } = Stopwatch.StartNew(); + + public int TimeStamp => StartTime.ToEOTimeStamp(Elapsed.ElapsedTicks); + } +} diff --git a/EOLib/misc.cs b/EOLib/misc.cs index 0f4d81fff..919ce6f62 100644 --- a/EOLib/misc.cs +++ b/EOLib/misc.cs @@ -20,8 +20,9 @@ public static T[] SubArray(this T[] arr, int offset, int count) public static class DateTimeExtension { - public static int ToEOTimeStamp(this DateTime dt) + public static int ToEOTimeStamp(this DateTime dt, long elapsedTicks) { + dt = dt.Add(TimeSpan.FromTicks(elapsedTicks)); return dt.Hour * 360000 + dt.Minute * 6000 + dt.Second * 100 + dt.Millisecond / 10; } } diff --git a/EndlessClient/GameExecution/EndlessGame.cs b/EndlessClient/GameExecution/EndlessGame.cs index 23c5cbdf1..801bc3d4c 100644 --- a/EndlessClient/GameExecution/EndlessGame.cs +++ b/EndlessClient/GameExecution/EndlessGame.cs @@ -21,6 +21,7 @@ using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using MonoGame.Extended.BitmapFonts; +using MonoGame.Extended.Input; namespace EndlessClient.GameExecution { @@ -111,6 +112,9 @@ protected override void Initialize() IsMouseVisible = true; IsFixedTimeStep = false; + + TargetElapsedTime = TimeSpan.FromMilliseconds(12); + _previousKeyState = Keyboard.GetState(); // setting Width/Height in window size repository applies the change to disable vsync @@ -175,17 +179,15 @@ protected override void LoadContent() protected override void Update(GameTime gameTime) { - // Force update at 60FPS - // Some game components rely on ~60FPS update times. See: https://github.com/ethanmoffat/EndlessClient/issues/199 + // Force updates to wait every 12ms + // Some game components rely on slower update times. 60FPS was the original, but 12ms factors nicely in 120ms "ticks" + // See: https://github.com/ethanmoffat/EndlessClient/issues/199 // Using IsFixedTimeStep = true with TargetUpdateTime set to 60FPS also limits the draw rate, which is not desired - if ((gameTime.TotalGameTime - _lastFrameUpdate).TotalMilliseconds > 1000.0 / 60) + if ((gameTime.TotalGameTime - _lastFrameUpdate).TotalMilliseconds >= 12.0) { - #if DEBUG - //todo: this is a debug-only mode launched with the F5 key. - //todo: move this to be handled by some sort of key listener once function keys are handled in-game var currentKeyState = Keyboard.GetState(); - if (_previousKeyState.IsKeyDown(Keys.F5) && currentKeyState.IsKeyUp(Keys.F5)) + if (KeyboardExtended.GetState().WasKeyJustDown(Keys.F5)) { _testModeLauncher.LaunchTestMode(); } diff --git a/EndlessClient/Rendering/Character/CharacterAnimator.cs b/EndlessClient/Rendering/Character/CharacterAnimator.cs index ec23be281..c97104075 100644 --- a/EndlessClient/Rendering/Character/CharacterAnimator.cs +++ b/EndlessClient/Rendering/Character/CharacterAnimator.cs @@ -1,7 +1,6 @@ using EndlessClient.GameExecution; using EndlessClient.HUD; using EndlessClient.HUD.Spells; -using EndlessClient.Input; using EOLib; using EOLib.Domain.Character; using EOLib.Domain.Extensions; @@ -20,9 +19,9 @@ namespace EndlessClient.Rendering.Character { public class CharacterAnimator : GameComponent, ICharacterAnimator { - public const int WALK_FRAME_TIME_MS = 125; - public const int ATTACK_FRAME_TIME_MS = 125; - public const int EMOTE_FRAME_TIME_MS = 250; + public const int WALK_FRAME_TIME_MS = 96; + public const int ATTACK_FRAME_TIME_MS = 108; + public const int FRAME_TIME_MS = 120; private readonly ICharacterRepository _characterRepository; private readonly ICurrentMapStateRepository _currentMapStateRepository; @@ -481,7 +480,7 @@ private void AnimateCharacterSpells() { _mainPlayerStartShoutTime.MatchSome(t => { - if (t.ElapsedMilliseconds >= (_shoutSpellData.CastTime - 1) * 480 + 350) + if (t.ElapsedMilliseconds >= _shoutSpellData.CastTime * 480) { _otherPlayerStartSpellCastTimes.Add(_characterRepository.MainCharacter.ID, new RenderFrameActionTime(_characterRepository.MainCharacter.ID)); _characterActions.CastSpell(_shoutSpellData.ID, _spellTarget); @@ -495,7 +494,7 @@ private void AnimateCharacterSpells() var playersDoneCasting = new HashSet(); foreach (var pair in _otherPlayerStartSpellCastTimes.Values) { - if (pair.ActionTimer.ElapsedMilliseconds >= ATTACK_FRAME_TIME_MS) + if (pair.ActionTimer.ElapsedMilliseconds >= FRAME_TIME_MS) { GetCurrentCharacterFromRepository(pair).Match( none: () => playersDoneCasting.Add(pair.UniqueID), @@ -527,7 +526,7 @@ private void AnimateCharacterEmotes() var playersDoneEmoting = new HashSet(); foreach (var pair in _startEmoteTimes.Values) { - if (pair.ActionTimer.ElapsedMilliseconds >= EMOTE_FRAME_TIME_MS) + if (pair.ActionTimer.ElapsedMilliseconds >= FRAME_TIME_MS * 2) { GetCurrentCharacterFromRepository(pair).Match( none: () => playersDoneEmoting.Add(pair.UniqueID), diff --git a/EndlessClient/Rendering/FixedTimeStepRepository.cs b/EndlessClient/Rendering/FixedTimeStepRepository.cs index a19896933..2714c7d3b 100644 --- a/EndlessClient/Rendering/FixedTimeStepRepository.cs +++ b/EndlessClient/Rendering/FixedTimeStepRepository.cs @@ -6,13 +6,13 @@ namespace EndlessClient.Rendering [AutoMappedType(IsSingleton = true)] public class FixedTimeStepRepository : IFixedTimeStepRepository { - private const int FIXED_UPDATE_TIME_MS = 24; // 40 FPS (walk updates at 10 FPS) + private const int FIXED_UPDATE_TIME_MS = 10; // 100 FPS (walk updates at 50 FPS) private int _isWalkUpdate; public Stopwatch FixedUpdateTimer { get; set; } - public bool IsUpdateFrame => FixedUpdateTimer.ElapsedMilliseconds > FIXED_UPDATE_TIME_MS; + public bool IsUpdateFrame => FixedUpdateTimer.ElapsedMilliseconds >= FIXED_UPDATE_TIME_MS; public bool IsWalkUpdateFrame => IsUpdateFrame && _isWalkUpdate == 3;