From 8562022b4dea84b3e7d4a15421a0a4f29cf4b832 Mon Sep 17 00:00:00 2001 From: Skipcast Date: Sat, 15 Jan 2022 16:51:22 +0100 Subject: [PATCH 1/5] Implemented animations for remote players --- .../Components/RemotePlayerInterpolator.cs | 46 ++++++- .../Game/Entities/FakePlayer.cs | 2 + .../Game/Player/Animations/SpriteAnimation.cs | 124 ++++++++++++++++++ 3 files changed, 169 insertions(+), 3 deletions(-) create mode 100644 JKMP.Plugin.Multiplayer/Game/Player/Animations/SpriteAnimation.cs diff --git a/JKMP.Plugin.Multiplayer/Game/Components/RemotePlayerInterpolator.cs b/JKMP.Plugin.Multiplayer/Game/Components/RemotePlayerInterpolator.cs index d11f68d..54355ec 100644 --- a/JKMP.Plugin.Multiplayer/Game/Components/RemotePlayerInterpolator.cs +++ b/JKMP.Plugin.Multiplayer/Game/Components/RemotePlayerInterpolator.cs @@ -4,6 +4,7 @@ using JKMP.Core.Logging; using JKMP.Plugin.Multiplayer.Game.Entities; using JKMP.Plugin.Multiplayer.Game.Player; +using JKMP.Plugin.Multiplayer.Game.Player.Animations; using JKMP.Plugin.Multiplayer.Game.Sound; using JKMP.Plugin.Multiplayer.Networking.Messages; using JumpKing; @@ -24,15 +25,42 @@ public class RemotePlayerInterpolator : Component private FakePlayer FakePlayer => (FakePlayer)gameObject; + private static readonly SpriteAnimation StretchAnimation = new(CreateStretchAnimationFrames()); + + private static IEnumerable CreateStretchAnimationFrames() + { + for (int i = 0; i < 7; ++i) + { + yield return new SpriteFrame(JKContentManager.PlayerSprites.stretch_one, 0.5f); + yield return new SpriteFrame(JKContentManager.PlayerSprites.stretch_smear, 0.25f); + yield return new SpriteFrame(JKContentManager.PlayerSprites.stretch_two, 0.5f); + yield return new SpriteFrame(JKContentManager.PlayerSprites.stretch_smear, 0.25f); + } + } + + private static readonly SpriteAnimation IdleAnimation = new( + new SpriteFrame(JKContentManager.PlayerSprites.idle, 7f, 15f), + new SpriteFrame(JKContentManager.PlayerSprites.look_up, 5f, 7f), + new SpriteFrame(JKContentManager.PlayerSprites.idle, 7f, 15f), + new SpriteFrame(StretchAnimation, 10.5f) + ); + + private static readonly SpriteAnimation WalkAnimation = new( + new SpriteFrame(JKContentManager.PlayerSprites.walk_one, 0.225f), + new SpriteFrame(JKContentManager.PlayerSprites.walk_smear, 0.05f), + new SpriteFrame(JKContentManager.PlayerSprites.walk_two, 0.225f), + new SpriteFrame(JKContentManager.PlayerSprites.walk_smear, 0.05f) + ); + private static readonly Dictionary StateSprites = new() { - { PlayerState.Idle, JKContentManager.PlayerSprites.idle }, + { PlayerState.Idle, IdleAnimation }, { PlayerState.Falling, JKContentManager.PlayerSprites.jump_fall }, { PlayerState.StartJump, JKContentManager.PlayerSprites.jump_charge }, { PlayerState.Jump, JKContentManager.PlayerSprites.jump_up }, { PlayerState.Knocked, JKContentManager.PlayerSprites.jump_bounce }, - { PlayerState.Land, JKContentManager.PlayerSprites.idle }, - { PlayerState.Walk, JKContentManager.PlayerSprites.walk_one }, + { PlayerState.Land, IdleAnimation }, + { PlayerState.Walk, WalkAnimation }, { PlayerState.Splat, JKContentManager.PlayerSprites.splat }, }; @@ -57,6 +85,12 @@ protected override void Update(float delta) { if (lastState != null && nextState != null) { + // Reset animation if we're changing states + if (FakePlayer.Sprite is SpriteAnimation spriteAnimation && lastState.State != nextState.State) + { + spriteAnimation.Reset(); + } + FakePlayer.SetSprite(StateSprites[lastState.State]); FakePlayer.SetDirection(nextState.WalkDirection); @@ -68,6 +102,12 @@ protected override void Update(float delta) elapsedTimeSinceLastState += delta; } + + if (FakePlayer.Sprite is SpriteAnimation) + { + var spriteAnimation = (SpriteAnimation)FakePlayer.Sprite; + spriteAnimation.Update(delta); + } } private void UpdateAudioEmitter() diff --git a/JKMP.Plugin.Multiplayer/Game/Entities/FakePlayer.cs b/JKMP.Plugin.Multiplayer/Game/Entities/FakePlayer.cs index d31e00e..5e0fbf9 100644 --- a/JKMP.Plugin.Multiplayer/Game/Entities/FakePlayer.cs +++ b/JKMP.Plugin.Multiplayer/Game/Entities/FakePlayer.cs @@ -12,6 +12,8 @@ namespace JKMP.Plugin.Multiplayer.Game.Entities { public class FakePlayer : Entity { + public Sprite Sprite => sprite; + private Sprite sprite; private bool flip; diff --git a/JKMP.Plugin.Multiplayer/Game/Player/Animations/SpriteAnimation.cs b/JKMP.Plugin.Multiplayer/Game/Player/Animations/SpriteAnimation.cs new file mode 100644 index 0000000..6d1df73 --- /dev/null +++ b/JKMP.Plugin.Multiplayer/Game/Player/Animations/SpriteAnimation.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using JKMP.Core.Logging; +using JumpKing; +using JumpKing.XnaWrappers; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace JKMP.Plugin.Multiplayer.Game.Player.Animations +{ + public class SpriteFrame + { + public Sprite Sprite { get; set; } + public float MinDuration { get; set; } + public float MaxDuration { get; set; } + + private static readonly Random Random = new(); + + public SpriteFrame(Sprite sprite, float minDuration, float? maxDuration = null) + { + Sprite = sprite; + MinDuration = minDuration; + MaxDuration = maxDuration ?? minDuration; + } + + public float GetRandomDuration() + { + if (MinDuration.Equals(MaxDuration)) + return MinDuration; + + return MinDuration + (float)Random.NextDouble() * (MaxDuration - MinDuration); + } + + public void Update(float dt) + { + if (Sprite is SpriteAnimation spriteAnimation) + { + spriteAnimation.Update(dt); + } + } + } + + public class SpriteAnimation : Sprite + { + private SpriteFrame[] frames; + private float currentDuration; + + private int currentFrameIndex; + private float currentFrameDuration; + + private SpriteFrame CurrentFrame => frames[currentFrameIndex]; + private Sprite CurrentSprite => CurrentFrame.Sprite; + + public SpriteAnimation(IEnumerable frames) + { + this.frames = frames.ToArray(); + + if (this.frames.Length == 0) + throw new ArgumentException("Animation must have at least one frame", nameof(frames)); + + currentFrameDuration = CurrentFrame.GetRandomDuration(); + } + + public SpriteAnimation(params SpriteFrame[] frames) : this(frames.AsEnumerable()) + { + } + + public SpriteAnimation(ICollection frames) : this(frames.AsEnumerable()) + { + } + + public void Reset() + { + currentFrameIndex = 0; + currentFrameDuration = CurrentFrame.GetRandomDuration(); + currentDuration = 0; + + foreach (var frame in frames) + { + if (frame.Sprite is SpriteAnimation spriteAnim) + spriteAnim.Reset(); + } + } + + public void Update(float dt) + { + currentDuration += dt; + + if (currentDuration >= currentFrameDuration) + { + currentFrameIndex += 1; + + if (currentFrameIndex >= frames.Length) + currentFrameIndex = 0; + + currentFrameDuration = CurrentFrame.GetRandomDuration(); + currentDuration = 0; + } + + CurrentFrame.Update(dt); + } + + public override void Draw(Vector2 pos, SpriteEffects effect = SpriteEffects.None) + { + CurrentSprite.Draw(pos, effect); + } + + public override void Draw(Rectangle dst, SpriteEffects effect = SpriteEffects.None) + { + CurrentSprite.Draw(dst, effect); + } + + public override void Draw(float x, float y, SpriteEffects effect = SpriteEffects.None) + { + CurrentSprite.Draw(x, y, effect); + } + + public override void Draw(Point pos, SpriteEffects effect = SpriteEffects.None) + { + CurrentSprite.Draw(pos, effect); + } + } +} \ No newline at end of file From aae7de58563e8ce8e87b7da75597026b6a6c6768 Mon Sep 17 00:00:00 2001 From: Skipcast Date: Sat, 15 Jan 2022 17:16:57 +0100 Subject: [PATCH 2/5] Fixed remote player state not syncing correctly in certain cases --- .../Game/Components/PlayerStateTransmitter.cs | 5 ++++- .../Game/Player/LocalPlayerListener.cs | 11 +++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/JKMP.Plugin.Multiplayer/Game/Components/PlayerStateTransmitter.cs b/JKMP.Plugin.Multiplayer/Game/Components/PlayerStateTransmitter.cs index 195a036..fb7569c 100644 --- a/JKMP.Plugin.Multiplayer/Game/Components/PlayerStateTransmitter.cs +++ b/JKMP.Plugin.Multiplayer/Game/Components/PlayerStateTransmitter.cs @@ -21,6 +21,7 @@ public class PlayerStateTransmitter : Component private BodyComp? body; private float timeSinceTransmission; + private PlayerState lastState; private readonly P2PManager p2p; @@ -64,11 +65,13 @@ protected override void LateUpdate(float delta) { timeSinceTransmission += delta; - if (timeSinceTransmission >= TransmissionInterval) + if (timeSinceTransmission >= TransmissionInterval || listener!.CurrentState != lastState) { timeSinceTransmission = 0; SendState(); } + + lastState = listener!.CurrentState; } private void SendState() diff --git a/JKMP.Plugin.Multiplayer/Game/Player/LocalPlayerListener.cs b/JKMP.Plugin.Multiplayer/Game/Player/LocalPlayerListener.cs index 5fd717f..290dfcc 100644 --- a/JKMP.Plugin.Multiplayer/Game/Player/LocalPlayerListener.cs +++ b/JKMP.Plugin.Multiplayer/Game/Player/LocalPlayerListener.cs @@ -43,7 +43,7 @@ public LocalPlayerListener() public void Update(float delta) { - if (localBody.LastVelocity.Y <= 0 && localBody.velocity.Y > 0) + if (localBody.LastVelocity.Y <= PlayerValues.GRAVITY && localBody.velocity.Y > PlayerValues.GRAVITY) { CurrentState = PlayerState.Falling; StartedFalling?.Invoke(localBody.IsKnocked); @@ -96,7 +96,7 @@ public void Update(float delta) if (walkDirection != lastWalkDirection) { Walk?.Invoke(walkDirection); - CurrentState = PlayerState.Walk; + CurrentState = walkDirection != 0 ? PlayerState.Walk : PlayerState.Idle; } WalkDirection = (sbyte)walkDirection; @@ -141,6 +141,13 @@ private void OnJump() { CurrentState = PlayerState.Jump; Jump?.Invoke(); + + WalkDirection = localBody.velocity.X switch + { + < 0 => -1, + > 0 => 1, + _ => 0 + }; } } } \ No newline at end of file From 34d973c39d5c5db016c2ab2a711e6e12ad4de3cc Mon Sep 17 00:00:00 2001 From: Skipcast Date: Sat, 15 Jan 2022 17:45:32 +0100 Subject: [PATCH 3/5] Fixed player name and message labels not being destroyed when the player disconnects --- .../Game/Components/FollowingTextRenderer.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/JKMP.Plugin.Multiplayer/Game/Components/FollowingTextRenderer.cs b/JKMP.Plugin.Multiplayer/Game/Components/FollowingTextRenderer.cs index 1103bf8..3e78dd6 100644 --- a/JKMP.Plugin.Multiplayer/Game/Components/FollowingTextRenderer.cs +++ b/JKMP.Plugin.Multiplayer/Game/Components/FollowingTextRenderer.cs @@ -72,6 +72,11 @@ public FollowingTextRenderer(SpriteFont font) textRenderer = new(font); } + protected override void OnOwnerDestroy() + { + textRenderer.Destroy(); + } + protected override void Init() { transform = GetComponent(); From dee6b9cfbd25609e9096f241225b46a339280fcb Mon Sep 17 00:00:00 2001 From: Skipcast Date: Sat, 15 Jan 2022 21:42:11 +0100 Subject: [PATCH 4/5] Fixed crash that occured when attempting to connect to a non friend player --- .../JKMP.Plugin.Multiplayer.csproj | 2 +- JKMP.Plugin.Multiplayer/Steam/SteamUtil.cs | 11 ++--------- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/JKMP.Plugin.Multiplayer/JKMP.Plugin.Multiplayer.csproj b/JKMP.Plugin.Multiplayer/JKMP.Plugin.Multiplayer.csproj index 8c5549a..604df1a 100644 --- a/JKMP.Plugin.Multiplayer/JKMP.Plugin.Multiplayer.csproj +++ b/JKMP.Plugin.Multiplayer/JKMP.Plugin.Multiplayer.csproj @@ -17,7 +17,7 @@ x86 - + diff --git a/JKMP.Plugin.Multiplayer/Steam/SteamUtil.cs b/JKMP.Plugin.Multiplayer/Steam/SteamUtil.cs index 0fd324e..d23f80a 100644 --- a/JKMP.Plugin.Multiplayer/Steam/SteamUtil.cs +++ b/JKMP.Plugin.Multiplayer/Steam/SteamUtil.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using JKMP.Core.Logging; using Steamworks; namespace JKMP.Plugin.Multiplayer.Steam @@ -29,15 +30,7 @@ private static void OnPersonaStateChange(Friend friend) { if (!SteamFriends.RequestUserInformation(steamId)) { - Friend friend = SteamFriends.GetFriends().FirstOrDefault(f => f.Id == steamId); - - if (!friend.Id.IsValid) - friend = SteamFriends.GetFriendsOnGameServer().FirstOrDefault(f => f.Id == steamId); - - if (!friend.Id.IsValid) - return null; - - return friend; + return new Friend(steamId); } var tcs = new TaskCompletionSource(); From 0be9eb6758db054d494783f4650e44e20d68e4a3 Mon Sep 17 00:00:00 2001 From: Skipcast Date: Sat, 15 Jan 2022 21:43:04 +0100 Subject: [PATCH 5/5] Run steam events async as well as on game thread to prevent permanent game freezes (hopefully) --- JKMP.Plugin.Multiplayer/Steam/SteamManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/JKMP.Plugin.Multiplayer/Steam/SteamManager.cs b/JKMP.Plugin.Multiplayer/Steam/SteamManager.cs index bdbcbdf..20c261f 100644 --- a/JKMP.Plugin.Multiplayer/Steam/SteamManager.cs +++ b/JKMP.Plugin.Multiplayer/Steam/SteamManager.cs @@ -15,7 +15,7 @@ public static bool InitializeSteam() { try { - SteamClient.Init(1061090, false); + SteamClient.Init(1061090, true); } catch (Exception ex) {