Add platformer locomotion slice
This commit is contained in:
@@ -15,4 +15,4 @@ public sealed record GameDefinition
|
||||
public LevelDefinition Level { get; init; }
|
||||
|
||||
public ImmutableArray<PlayerDefinition> Players { get; init; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,11 +6,12 @@ namespace SideScrollerGame.Sim.Definitions;
|
||||
[ExcludeFromCodeCoverage]
|
||||
public sealed record LevelDefinition
|
||||
{
|
||||
public LevelDefinition(AxisAlignedBounds worldBounds, ImmutableArray<HazardDefinition> hazards, ImmutableArray<TriggerDefinition> triggers)
|
||||
public LevelDefinition(AxisAlignedBounds worldBounds, ImmutableArray<HazardDefinition> hazards, ImmutableArray<TriggerDefinition> triggers, ImmutableArray<SolidPlatformDefinition> platforms)
|
||||
{
|
||||
WorldBounds = worldBounds;
|
||||
Hazards = hazards.IsDefault ? ImmutableArray<HazardDefinition>.Empty : hazards;
|
||||
Triggers = triggers.IsDefault ? ImmutableArray<TriggerDefinition>.Empty : triggers;
|
||||
Platforms = platforms.IsDefault ? ImmutableArray<SolidPlatformDefinition>.Empty : platforms;
|
||||
}
|
||||
|
||||
public AxisAlignedBounds WorldBounds { get; init; }
|
||||
@@ -18,4 +19,6 @@ public sealed record LevelDefinition
|
||||
public ImmutableArray<HazardDefinition> Hazards { get; init; }
|
||||
|
||||
public ImmutableArray<TriggerDefinition> Triggers { get; init; }
|
||||
|
||||
public ImmutableArray<SolidPlatformDefinition> Platforms { get; init; }
|
||||
}
|
||||
@@ -6,11 +6,17 @@ namespace SideScrollerGame.Sim.Definitions;
|
||||
[ExcludeFromCodeCoverage]
|
||||
public sealed record PlayerDefinition
|
||||
{
|
||||
public PlayerDefinition(PlayerId playerId, FixPointVector2 spawnPosition, int maxHealth)
|
||||
public PlayerDefinition(PlayerId playerId, FixPointVector2 spawnPosition, int maxHealth, bool usesPlatformerMotion = false, FixPoint16 moveSpeedPerTick = default, FixPoint16 gravityPerTick = default, FixPoint16 jumpVelocityPerTick = default, int coyoteTicks = 0, int jumpBufferTicks = 0)
|
||||
{
|
||||
PlayerId = playerId;
|
||||
SpawnPosition = spawnPosition;
|
||||
MaxHealth = maxHealth;
|
||||
UsesPlatformerMotion = usesPlatformerMotion;
|
||||
MoveSpeedPerTick = moveSpeedPerTick;
|
||||
GravityPerTick = gravityPerTick;
|
||||
JumpVelocityPerTick = jumpVelocityPerTick;
|
||||
CoyoteTicks = coyoteTicks;
|
||||
JumpBufferTicks = jumpBufferTicks;
|
||||
}
|
||||
|
||||
public PlayerId PlayerId { get; init; }
|
||||
@@ -18,4 +24,16 @@ public sealed record PlayerDefinition
|
||||
public FixPointVector2 SpawnPosition { get; init; }
|
||||
|
||||
public int MaxHealth { get; init; }
|
||||
|
||||
public bool UsesPlatformerMotion { get; init; }
|
||||
|
||||
public FixPoint16 MoveSpeedPerTick { get; init; }
|
||||
|
||||
public FixPoint16 GravityPerTick { get; init; }
|
||||
|
||||
public FixPoint16 JumpVelocityPerTick { get; init; }
|
||||
|
||||
public int CoyoteTicks { get; init; }
|
||||
|
||||
public int JumpBufferTicks { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace SideScrollerGame.Sim.Definitions;
|
||||
|
||||
[ExcludeFromCodeCoverage]
|
||||
public sealed record SolidPlatformDefinition
|
||||
{
|
||||
public SolidPlatformDefinition(string id, AxisAlignedBounds bounds)
|
||||
{
|
||||
Id = id;
|
||||
Bounds = bounds;
|
||||
}
|
||||
|
||||
public string Id { get; init; }
|
||||
|
||||
public AxisAlignedBounds Bounds { get; init; }
|
||||
}
|
||||
@@ -6,13 +6,15 @@ namespace SideScrollerGame.Sim.Runtime;
|
||||
[ExcludeFromCodeCoverage]
|
||||
public sealed record PlayerSnapshot
|
||||
{
|
||||
public PlayerSnapshot(PlayerId playerId, FixPointVector2 position, sbyte moveAxisX, sbyte moveAxisY, int health)
|
||||
public PlayerSnapshot(PlayerId playerId, FixPointVector2 position, sbyte moveAxisX, sbyte moveAxisY, int health, FixPoint16 verticalVelocity, bool isGrounded)
|
||||
{
|
||||
PlayerId = playerId;
|
||||
Position = position;
|
||||
MoveAxisX = moveAxisX;
|
||||
MoveAxisY = moveAxisY;
|
||||
Health = health;
|
||||
VerticalVelocity = verticalVelocity;
|
||||
IsGrounded = isGrounded;
|
||||
}
|
||||
|
||||
public PlayerId PlayerId { get; init; }
|
||||
@@ -24,4 +26,8 @@ public sealed record PlayerSnapshot
|
||||
public sbyte MoveAxisY { get; init; }
|
||||
|
||||
public int Health { get; init; }
|
||||
|
||||
public FixPoint16 VerticalVelocity { get; init; }
|
||||
|
||||
public bool IsGrounded { get; init; }
|
||||
}
|
||||
@@ -5,7 +5,7 @@ namespace SideScrollerGame.Sim.Runtime;
|
||||
|
||||
public sealed class PlayerState
|
||||
{
|
||||
public PlayerState(PlayerId playerId, FixPointVector2 position, sbyte moveAxisX, sbyte moveAxisY, short aimAxisX, short aimAxisY, int selectedWeaponSlot, int buttonMask, int health)
|
||||
public PlayerState(PlayerId playerId, FixPointVector2 position, sbyte moveAxisX, sbyte moveAxisY, short aimAxisX, short aimAxisY, int selectedWeaponSlot, int buttonMask, int health, FixPoint16 verticalVelocity, bool isGrounded, int lastGroundedTick, int bufferedJumpTick)
|
||||
{
|
||||
PlayerId = playerId;
|
||||
Position = position;
|
||||
@@ -16,11 +16,15 @@ public sealed class PlayerState
|
||||
SelectedWeaponSlot = selectedWeaponSlot;
|
||||
ButtonMask = buttonMask;
|
||||
Health = health;
|
||||
VerticalVelocity = verticalVelocity;
|
||||
IsGrounded = isGrounded;
|
||||
LastGroundedTick = lastGroundedTick;
|
||||
BufferedJumpTick = bufferedJumpTick;
|
||||
}
|
||||
|
||||
public PlayerState Clone()
|
||||
{
|
||||
return new(PlayerId, Position, MoveAxisX, MoveAxisY, AimAxisX, AimAxisY, SelectedWeaponSlot, ButtonMask, Health);
|
||||
return new(PlayerId, Position, MoveAxisX, MoveAxisY, AimAxisX, AimAxisY, SelectedWeaponSlot, ButtonMask, Health, VerticalVelocity, IsGrounded, LastGroundedTick, BufferedJumpTick);
|
||||
}
|
||||
|
||||
public void SetMoveAxis(sbyte x, sbyte y)
|
||||
@@ -46,6 +50,11 @@ public sealed class PlayerState
|
||||
SelectedWeaponSlot = slotIndex;
|
||||
}
|
||||
|
||||
public bool IsButtonPressed(InputButton button)
|
||||
{
|
||||
return (ButtonMask & (1 << (int)button)) != 0;
|
||||
}
|
||||
|
||||
public void Advance()
|
||||
{
|
||||
Position += new FixPointVector2(MoveAxisX, MoveAxisY);
|
||||
@@ -56,11 +65,46 @@ public sealed class PlayerState
|
||||
Health = System.Math.Max(0, Health - damage);
|
||||
}
|
||||
|
||||
public void BufferJump(int tick)
|
||||
{
|
||||
BufferedJumpTick = tick;
|
||||
}
|
||||
|
||||
public bool HasBufferedJump(int tick, int jumpBufferTicks)
|
||||
{
|
||||
return BufferedJumpTick >= 0 && tick - BufferedJumpTick <= jumpBufferTicks;
|
||||
}
|
||||
|
||||
public void ConsumeBufferedJump()
|
||||
{
|
||||
BufferedJumpTick = -1;
|
||||
}
|
||||
|
||||
public void SetPosition(FixPointVector2 position)
|
||||
{
|
||||
Position = position;
|
||||
}
|
||||
|
||||
public void SetVerticalVelocity(FixPoint16 verticalVelocity)
|
||||
{
|
||||
VerticalVelocity = verticalVelocity;
|
||||
}
|
||||
|
||||
public void SetGrounded(bool isGrounded, int tick)
|
||||
{
|
||||
IsGrounded = isGrounded;
|
||||
if (isGrounded)
|
||||
LastGroundedTick = tick;
|
||||
}
|
||||
|
||||
public void LeaveGround(int tick)
|
||||
{
|
||||
if (IsGrounded)
|
||||
LastGroundedTick = tick;
|
||||
|
||||
IsGrounded = false;
|
||||
}
|
||||
|
||||
public PlayerId PlayerId { get; }
|
||||
|
||||
public FixPointVector2 Position { get; private set; }
|
||||
@@ -78,4 +122,12 @@ public sealed class PlayerState
|
||||
public int ButtonMask { get; private set; }
|
||||
|
||||
public int Health { get; private set; }
|
||||
|
||||
public FixPoint16 VerticalVelocity { get; private set; }
|
||||
|
||||
public bool IsGrounded { get; private set; }
|
||||
|
||||
public int LastGroundedTick { get; private set; }
|
||||
|
||||
public int BufferedJumpTick { get; private set; }
|
||||
}
|
||||
@@ -26,6 +26,18 @@ internal static class GameDefinitionHasher
|
||||
public int SpawnY { get; init; }
|
||||
|
||||
public int MaxHealth { get; init; }
|
||||
|
||||
public bool UsesPlatformerMotion { get; init; }
|
||||
|
||||
public int MoveSpeedPerTick { get; init; }
|
||||
|
||||
public int GravityPerTick { get; init; }
|
||||
|
||||
public int JumpVelocityPerTick { get; init; }
|
||||
|
||||
public int CoyoteTicks { get; init; }
|
||||
|
||||
public int JumpBufferTicks { get; init; }
|
||||
}
|
||||
|
||||
[ExcludeFromCodeCoverage]
|
||||
@@ -36,6 +48,8 @@ internal static class GameDefinitionHasher
|
||||
public ImmutableArray<HazardDefinitionDocument> Hazards { get; init; }
|
||||
|
||||
public ImmutableArray<TriggerDefinitionDocument> Triggers { get; init; }
|
||||
|
||||
public ImmutableArray<SolidPlatformDefinitionDocument> Platforms { get; init; }
|
||||
}
|
||||
|
||||
[ExcludeFromCodeCoverage]
|
||||
@@ -70,6 +84,14 @@ internal static class GameDefinitionHasher
|
||||
public string Kind { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
[ExcludeFromCodeCoverage]
|
||||
private sealed record SolidPlatformDefinitionDocument
|
||||
{
|
||||
public string Id { get; init; } = string.Empty;
|
||||
|
||||
public BoundsDocument Bounds { get; init; } = null!;
|
||||
}
|
||||
|
||||
public static int Compute(GameDefinition gameDefinition)
|
||||
{
|
||||
List<PlayerDefinitionDocument> players = new(gameDefinition.Players.Length);
|
||||
@@ -80,7 +102,13 @@ internal static class GameDefinitionHasher
|
||||
PlayerId = player.PlayerId.Value,
|
||||
SpawnX = player.SpawnPosition.m_X.m_Value,
|
||||
SpawnY = player.SpawnPosition.m_Y.m_Value,
|
||||
MaxHealth = player.MaxHealth
|
||||
MaxHealth = player.MaxHealth,
|
||||
UsesPlatformerMotion = player.UsesPlatformerMotion,
|
||||
MoveSpeedPerTick = player.MoveSpeedPerTick.m_Value,
|
||||
GravityPerTick = player.GravityPerTick.m_Value,
|
||||
JumpVelocityPerTick = player.JumpVelocityPerTick.m_Value,
|
||||
CoyoteTicks = player.CoyoteTicks,
|
||||
JumpBufferTicks = player.JumpBufferTicks
|
||||
});
|
||||
}
|
||||
|
||||
@@ -106,13 +134,24 @@ internal static class GameDefinitionHasher
|
||||
});
|
||||
}
|
||||
|
||||
List<SolidPlatformDefinitionDocument> platforms = new(gameDefinition.Level.Platforms.Length);
|
||||
foreach (var platform in gameDefinition.Level.Platforms)
|
||||
{
|
||||
platforms.Add(new()
|
||||
{
|
||||
Id = platform.Id,
|
||||
Bounds = ToDocument(platform.Bounds)
|
||||
});
|
||||
}
|
||||
|
||||
var bytes = JsonSerializer.SerializeToUtf8Bytes(new GameDefinitionDocument
|
||||
{
|
||||
Level = new()
|
||||
{
|
||||
WorldBounds = ToDocument(gameDefinition.Level.WorldBounds),
|
||||
Hazards = hazards.ToImmutableArray(),
|
||||
Triggers = triggers.ToImmutableArray()
|
||||
Triggers = triggers.ToImmutableArray(),
|
||||
Platforms = platforms.ToImmutableArray()
|
||||
},
|
||||
Players = players.ToImmutableArray()
|
||||
});
|
||||
|
||||
@@ -49,6 +49,14 @@ internal static class SimulationStateSerializer
|
||||
public int ButtonMask { get; init; }
|
||||
|
||||
public int Health { get; init; }
|
||||
|
||||
public int VerticalVelocity { get; init; }
|
||||
|
||||
public bool IsGrounded { get; init; }
|
||||
|
||||
public int LastGroundedTick { get; init; }
|
||||
|
||||
public int BufferedJumpTick { get; init; }
|
||||
}
|
||||
|
||||
public static byte[] Serialize(SimulationState state)
|
||||
@@ -67,7 +75,11 @@ internal static class SimulationStateSerializer
|
||||
AimAxisY = player.AimAxisY,
|
||||
SelectedWeaponSlot = player.SelectedWeaponSlot,
|
||||
ButtonMask = player.ButtonMask,
|
||||
Health = player.Health
|
||||
Health = player.Health,
|
||||
VerticalVelocity = player.VerticalVelocity.m_Value,
|
||||
IsGrounded = player.IsGrounded,
|
||||
LastGroundedTick = player.LastGroundedTick,
|
||||
BufferedJumpTick = player.BufferedJumpTick
|
||||
});
|
||||
}
|
||||
|
||||
@@ -91,7 +103,7 @@ internal static class SimulationStateSerializer
|
||||
|
||||
var players = ImmutableArray.CreateBuilder<PlayerState>(document.Players.Length);
|
||||
foreach (var player in document.Players)
|
||||
players.Add(new(new(player.PlayerId), new(new() { m_Value = player.PositionX }, new FixPoint16 { m_Value = player.PositionY }), player.MoveAxisX, player.MoveAxisY, player.AimAxisX, player.AimAxisY, player.SelectedWeaponSlot, player.ButtonMask, player.Health));
|
||||
players.Add(new(new(player.PlayerId), new(new() { m_Value = player.PositionX }, new FixPoint16 { m_Value = player.PositionY }), player.MoveAxisX, player.MoveAxisY, player.AimAxisX, player.AimAxisY, player.SelectedWeaponSlot, player.ButtonMask, player.Health, new() { m_Value = player.VerticalVelocity }, player.IsGrounded, player.LastGroundedTick, player.BufferedJumpTick));
|
||||
|
||||
return new(document.Tick, document.Seed, document.RandomState, document.LastRandomValue, players.MoveToImmutable(), document.ActivatedTriggerIds.ToImmutableHashSet(StringComparer.Ordinal));
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ public sealed class Simulation
|
||||
PreviousSnapshot = CurrentSnapshot;
|
||||
List<SimulationEvent> events = new();
|
||||
|
||||
ApplyActions(actions);
|
||||
ApplyActions(actions.Tick, actions);
|
||||
var nextRandomState = AdvanceRandom();
|
||||
AdvancePlayers(actions.Tick, events);
|
||||
ResolveBounds(actions.Tick, events);
|
||||
@@ -84,7 +84,10 @@ public sealed class Simulation
|
||||
|
||||
var players = ImmutableArray.CreateBuilder<PlayerState>(gameDefinition.Players.Length);
|
||||
foreach (var player in gameDefinition.Players)
|
||||
players.Add(new(player.PlayerId, player.SpawnPosition, 0, 0, 0, 0, 0, 0, player.MaxHealth));
|
||||
{
|
||||
var isGrounded = IsSupported(gameDefinition.Level, player.SpawnPosition);
|
||||
players.Add(new(player.PlayerId, player.SpawnPosition, 0, 0, 0, 0, 0, 0, player.MaxHealth, FixPoint16.Zero, isGrounded, isGrounded ? 0 : -1, -1));
|
||||
}
|
||||
|
||||
var normalizedSeed = NormalizeSeed(seed);
|
||||
return new(0, seed, normalizedSeed, 0, players.MoveToImmutable(), ImmutableHashSet<string>.Empty);
|
||||
@@ -108,6 +111,21 @@ public sealed class Simulation
|
||||
|
||||
if (!gameDefinition.Level.WorldBounds.Contains(player.SpawnPosition))
|
||||
throw new InvalidOperationException($"Player {player.PlayerId.Value} spawn must start inside world bounds.");
|
||||
|
||||
if (player.MoveSpeedPerTick < FixPoint16.Zero)
|
||||
throw new InvalidOperationException($"Player {player.PlayerId.Value} move speed must be non-negative.");
|
||||
|
||||
if (player.GravityPerTick < FixPoint16.Zero)
|
||||
throw new InvalidOperationException($"Player {player.PlayerId.Value} gravity must be non-negative.");
|
||||
|
||||
if (player.JumpVelocityPerTick < FixPoint16.Zero)
|
||||
throw new InvalidOperationException($"Player {player.PlayerId.Value} jump velocity must be non-negative.");
|
||||
|
||||
if (player.CoyoteTicks < 0)
|
||||
throw new InvalidOperationException($"Player {player.PlayerId.Value} coyote ticks must be non-negative.");
|
||||
|
||||
if (player.JumpBufferTicks < 0)
|
||||
throw new InvalidOperationException($"Player {player.PlayerId.Value} jump buffer ticks must be non-negative.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,12 +138,12 @@ public sealed class Simulation
|
||||
{
|
||||
var players = ImmutableArray.CreateBuilder<PlayerSnapshot>(state.Players.Length);
|
||||
foreach (var player in state.Players)
|
||||
players.Add(new(player.PlayerId, player.Position, player.MoveAxisX, player.MoveAxisY, player.Health));
|
||||
players.Add(new(player.PlayerId, player.Position, player.MoveAxisX, player.MoveAxisY, player.Health, player.VerticalVelocity, player.IsGrounded));
|
||||
|
||||
return new(state.Tick, stateHash, state.LastRandomValue, players.MoveToImmutable());
|
||||
}
|
||||
|
||||
private void ApplyActions(TickActionBatch actions)
|
||||
private void ApplyActions(int tick, TickActionBatch actions)
|
||||
{
|
||||
foreach (var action in actions.Actions)
|
||||
{
|
||||
@@ -138,7 +156,11 @@ public sealed class Simulation
|
||||
CurrentState.GetRequiredPlayer(aimAxisChanged.PlayerId).SetAimAxis(aimAxisChanged.X, aimAxisChanged.Y);
|
||||
break;
|
||||
case ButtonChanged buttonChanged:
|
||||
CurrentState.GetRequiredPlayer(buttonChanged.PlayerId).SetButton(buttonChanged.Button, buttonChanged.IsPressed);
|
||||
var player = CurrentState.GetRequiredPlayer(buttonChanged.PlayerId);
|
||||
var wasPressed = player.IsButtonPressed(buttonChanged.Button);
|
||||
player.SetButton(buttonChanged.Button, buttonChanged.IsPressed);
|
||||
if (buttonChanged.Button == InputButton.Jump && buttonChanged.IsPressed && !wasPressed)
|
||||
player.BufferJump(tick);
|
||||
break;
|
||||
case WeaponSlotSelected weaponSlotSelected:
|
||||
CurrentState.GetRequiredPlayer(weaponSlotSelected.PlayerId).SelectWeaponSlot(weaponSlotSelected.SlotIndex);
|
||||
@@ -153,7 +175,10 @@ public sealed class Simulation
|
||||
{
|
||||
foreach (var player in CurrentState.Players)
|
||||
{
|
||||
if (player.MoveAxisX != 0 || player.MoveAxisY != 0)
|
||||
var definition = GetPlayerDefinition(player.PlayerId);
|
||||
if (definition.UsesPlatformerMotion)
|
||||
AdvancePlatformerPlayer(player, definition, tick, events);
|
||||
else if (player.MoveAxisX != 0 || player.MoveAxisY != 0)
|
||||
{
|
||||
player.Advance();
|
||||
events.Add(new("PlayerMoved", tick, player.PlayerId));
|
||||
@@ -165,6 +190,9 @@ public sealed class Simulation
|
||||
{
|
||||
foreach (var player in CurrentState.Players)
|
||||
{
|
||||
if (GetPlayerDefinition(player.PlayerId).UsesPlatformerMotion)
|
||||
continue;
|
||||
|
||||
var clamped = m_GameDefinition.Level.WorldBounds.Clamp(player.Position);
|
||||
if (clamped != player.Position)
|
||||
{
|
||||
@@ -174,6 +202,135 @@ public sealed class Simulation
|
||||
}
|
||||
}
|
||||
|
||||
private void AdvancePlatformerPlayer(PlayerState player, PlayerDefinition definition, int tick, List<SimulationEvent> events)
|
||||
{
|
||||
var previousPosition = player.Position;
|
||||
var nextPosition = previousPosition;
|
||||
nextPosition.m_X += definition.MoveSpeedPerTick * player.MoveAxisX;
|
||||
|
||||
TryConsumeBufferedJump(player, definition, tick, events);
|
||||
|
||||
if (!player.IsGrounded || !player.VerticalVelocity.IsZero())
|
||||
player.SetVerticalVelocity(player.VerticalVelocity + definition.GravityPerTick);
|
||||
|
||||
nextPosition.m_Y += player.VerticalVelocity;
|
||||
|
||||
var clampedX = FixPoint16.Clamp(nextPosition.m_X, m_GameDefinition.Level.WorldBounds.Min.m_X, m_GameDefinition.Level.WorldBounds.Max.m_X);
|
||||
if (clampedX != nextPosition.m_X)
|
||||
{
|
||||
nextPosition.m_X = clampedX;
|
||||
events.Add(new("PlayerClamped", tick, player.PlayerId));
|
||||
}
|
||||
|
||||
ResolvePlatformerVerticalMovement(player, definition, previousPosition, ref nextPosition, tick, events);
|
||||
player.SetPosition(nextPosition);
|
||||
|
||||
if (player.Position != previousPosition)
|
||||
events.Add(new("PlayerMoved", tick, player.PlayerId));
|
||||
}
|
||||
|
||||
private void ResolvePlatformerVerticalMovement(PlayerState player, PlayerDefinition definition, FixPointVector2 previousPosition, ref FixPointVector2 nextPosition, int tick, List<SimulationEvent> events)
|
||||
{
|
||||
if (TryFindLandingY(previousPosition, nextPosition, out var landingY))
|
||||
{
|
||||
nextPosition.m_Y = landingY;
|
||||
player.SetVerticalVelocity(FixPoint16.Zero);
|
||||
if (!player.IsGrounded)
|
||||
events.Add(new("PlayerLanded", tick, player.PlayerId));
|
||||
|
||||
player.SetGrounded(true, tick);
|
||||
if (TryConsumeBufferedJump(player, definition, tick, events))
|
||||
{
|
||||
nextPosition.m_Y += player.VerticalVelocity;
|
||||
nextPosition.m_Y = FixPoint16.Max(nextPosition.m_Y, m_GameDefinition.Level.WorldBounds.Min.m_Y);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var clampedY = FixPoint16.Clamp(nextPosition.m_Y, m_GameDefinition.Level.WorldBounds.Min.m_Y, m_GameDefinition.Level.WorldBounds.Max.m_Y);
|
||||
if (clampedY != nextPosition.m_Y)
|
||||
{
|
||||
nextPosition.m_Y = clampedY;
|
||||
player.SetVerticalVelocity(FixPoint16.Zero);
|
||||
events.Add(new("PlayerClamped", tick, player.PlayerId));
|
||||
}
|
||||
|
||||
if (!IsSupported(m_GameDefinition.Level, nextPosition) && player.IsGrounded)
|
||||
player.LeaveGround(tick);
|
||||
}
|
||||
|
||||
private bool TryConsumeBufferedJump(PlayerState player, PlayerDefinition definition, int tick, List<SimulationEvent> events)
|
||||
{
|
||||
if (!player.HasBufferedJump(tick, definition.JumpBufferTicks))
|
||||
return false;
|
||||
|
||||
if (!player.IsGrounded && (player.LastGroundedTick < 0 || tick - player.LastGroundedTick > definition.CoyoteTicks))
|
||||
return false;
|
||||
|
||||
player.SetVerticalVelocity(-definition.JumpVelocityPerTick);
|
||||
player.SetGrounded(false, tick);
|
||||
player.ConsumeBufferedJump();
|
||||
events.Add(new("PlayerJumped", tick, player.PlayerId));
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryFindLandingY(FixPointVector2 previousPosition, FixPointVector2 nextPosition, out FixPoint16 landingY)
|
||||
{
|
||||
landingY = default;
|
||||
var found = false;
|
||||
var worldFloorY = m_GameDefinition.Level.WorldBounds.Max.m_Y;
|
||||
|
||||
if (previousPosition.m_Y <= worldFloorY && nextPosition.m_Y >= worldFloorY)
|
||||
{
|
||||
landingY = worldFloorY;
|
||||
found = true;
|
||||
}
|
||||
|
||||
foreach (var platform in m_GameDefinition.Level.Platforms)
|
||||
{
|
||||
var topY = platform.Bounds.Min.m_Y;
|
||||
if (previousPosition.m_Y > topY || nextPosition.m_Y < topY)
|
||||
continue;
|
||||
|
||||
if (nextPosition.m_X < platform.Bounds.Min.m_X || nextPosition.m_X > platform.Bounds.Max.m_X)
|
||||
continue;
|
||||
|
||||
if (!found || topY < landingY)
|
||||
{
|
||||
landingY = topY;
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
private static bool IsSupported(LevelDefinition levelDefinition, FixPointVector2 position)
|
||||
{
|
||||
if (position.m_Y == levelDefinition.WorldBounds.Max.m_Y)
|
||||
return true;
|
||||
|
||||
foreach (var platform in levelDefinition.Platforms)
|
||||
{
|
||||
if (position.m_Y == platform.Bounds.Min.m_Y && position.m_X >= platform.Bounds.Min.m_X && position.m_X <= platform.Bounds.Max.m_X)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private PlayerDefinition GetPlayerDefinition(PlayerId playerId)
|
||||
{
|
||||
foreach (var player in m_GameDefinition.Players)
|
||||
{
|
||||
if (player.PlayerId == playerId)
|
||||
return player;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"Unknown player id {playerId.Value}.");
|
||||
}
|
||||
|
||||
private void ResolveHazards(int tick, List<SimulationEvent> events)
|
||||
{
|
||||
foreach (var player in CurrentState.Players)
|
||||
|
||||
Reference in New Issue
Block a user