Skip to content

Commit

Permalink
Make animation timings wrok more consistently. Fixes:
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
ethanmoffat committed May 5, 2023
1 parent c795ced commit 5363746
Show file tree
Hide file tree
Showing 6 changed files with 54 additions and 24 deletions.
17 changes: 10 additions & 7 deletions EOLib/Domain/Character/CharacterActions.cs
Expand Up @@ -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)
Expand All @@ -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();
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -128,7 +131,7 @@ public void CastSpell(int spellId, ISpellTargetable target)
{
builder = builder
.AddShort(spellId)
.AddThree(DateTime.Now.ToEOTimeStamp());
.AddThree(_gameStartTimeProvider.TimeStamp);
}
else
{
Expand All @@ -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);
}
}

Expand Down
25 changes: 25 additions & 0 deletions 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);
}
}
3 changes: 2 additions & 1 deletion EOLib/misc.cs
Expand Up @@ -20,8 +20,9 @@ public static T[] SubArray<T>(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;
}
}
Expand Down
16 changes: 9 additions & 7 deletions EndlessClient/GameExecution/EndlessGame.cs
Expand Up @@ -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
{
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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();
}
Expand Down
13 changes: 6 additions & 7 deletions 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;
Expand All @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -495,7 +494,7 @@ private void AnimateCharacterSpells()
var playersDoneCasting = new HashSet<int>();
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),
Expand Down Expand Up @@ -527,7 +526,7 @@ private void AnimateCharacterEmotes()
var playersDoneEmoting = new HashSet<int>();
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),
Expand Down
4 changes: 2 additions & 2 deletions EndlessClient/Rendering/FixedTimeStepRepository.cs
Expand Up @@ -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;

Expand Down

0 comments on commit 5363746

Please sign in to comment.