Skip to content

Commit

Permalink
Add jumper spin, rewrite some logic (#92)
Browse files Browse the repository at this point in the history
* Remove unused method

This method had no references, I don't know what was the origin of this, probably some early Jumper color overlay(?)

* Retrieve the node once, extract and refactor animation switch

* Migrate sprite rotation from new codebase

* Extract and reorder methods

* Extract physics methods, change sprite flipping logic

* Extract physics methods, change sprite flipping logic

* Adjust the clamp

* Update comment

* Rename variables

* Format, reorder methods

* Fix typo

* Remove unused flag, reorder methods

* Reorder built-ins, cache components, set player data on _Ready

Rather than accessing the nodes all the time through GetNode, just cache them once on _Ready and reuse.

This also changes when the player-specific Setters are fired, because when we call Init on join, the node has not really entered the tree yet, so the components were not ready to use.

* Add jumping hotkey

* Format code, switch to cached components
  • Loading branch information
DarkStoorM authored Apr 12, 2024
1 parent a996486 commit 8142ea6
Show file tree
Hide file tree
Showing 2 changed files with 141 additions and 105 deletions.
10 changes: 10 additions & 0 deletions JumpRoyale/src/Arena.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,16 @@ public override void _UnhandledInput(InputEvent @event)
commandHandler.SpawnFakePlayers();
}
}

// Make everyone jump, no exceptions
if (Input.IsPhysicalKeyPressed(Key.J))
{
foreach (Jumper jumper in ActiveJumpers.Instance.AllJumpers())
{
jumper.RandomJump();
jumper.FlashPlayerName();
}
}
}

/// <summary>
Expand Down
236 changes: 131 additions & 105 deletions JumpRoyale/src/Jumper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,16 @@ public partial class Jumper : CharacterBody2D
/// </summary>
private readonly HashSet<Vector2> _recentPosition = [];

private AnimatedSprite2D _animatedSprite2D = null!;
private RichTextLabel _nameLabel = null!;
private CpuParticles2D _cpuParticles2D = null!;

/// <summary>
/// Used to block the fadeout in some situations, e.g. at the start of the game. This is automatically set to true
/// on every jump.
/// </summary>
private bool _canFadePlayerName;
private bool _lastJumpZeroAngle;
private bool _wasOnFloor;

// Get the gravity from the project settings to be synced with RigidBody nodes.
private float _gravity = ProjectSettings.GetSetting("physics/2d/default_gravity").AsSingle();
Expand Down Expand Up @@ -53,13 +56,37 @@ public partial class Jumper : CharacterBody2D
public void Init(int x, int y, [NotNull] PlayerData playerData)
{
PlayerData = playerData;

Position = new Vector2(x, y);
Name = PlayerData.Name;
}

public override void _Ready()
{
_animatedSprite2D = GetNode<AnimatedSprite2D>(SpriteNodeName);
_nameLabel = GetNode<RichTextLabel>(NameNodeName);
_cpuParticles2D = GetNode<CpuParticles2D>(ParticleSystemNodeName);

SetCharacter();
SetPlayerName();
SetGlow();

_animatedSprite2D.AnimationFinished += OnSpriteAnimationFinished;
}

public override void _PhysicsProcess(double delta)
{
StopOnFloor();
ApplyInitialGravity(delta);
ApplyJumpVelocity();
RotateInAir(delta);
BounceOffWall();
PlayNotGroundedAnimation();
UpdateNameTransparency();

_previousXVelocity = Velocity.X;

MoveAndSlide();
StorePosition();
}

/// <summary>
Expand All @@ -71,34 +98,33 @@ public void SetPlayerName()
// Note: ToHTML() excludes alpha component to avoid transparent names
string colorCode = Color.FromString(PlayerData.PlayerNameColor, GameConstants.DefaultNameColor).ToHtml(false);

RichTextLabel nameLabel = GetNode<RichTextLabel>(NameNodeName);

nameLabel.Text = $"[center][color={colorCode}]{PlayerData.Name}[/color][/center]";
_nameLabel.Text = $"[center][color={colorCode}]{PlayerData.Name}[/color][/center]";
}

public void SetCrazyParticles()
{
CpuParticles2D particles = GetGlowNode();

// Make sure we can repeatedly call this function without unbounded growth.
particles.Amount = Math.Min(particles.Amount * 5, 500);
_cpuParticles2D.Amount = Math.Min(_cpuParticles2D.Amount * 5, 500);
}

public void SetCharacter()
{
AnimatedSprite2D sprite = GetNode<AnimatedSprite2D>(SpriteNodeName);
int choice = PlayerData.CharacterChoice;
string gender = choice > 9 ? "f" : "m";
int charNumber = ((choice - 1) % 9 / 3) + 1;
int clothingNumber = ((choice - 1) % 3) + 1;

GD.Print("Choice: " + choice + " Gender: " + gender + " Char: " + charNumber + " Clothing: " + clothingNumber);

sprite.SpriteFrames = SpriteFrameCreator.Instance.GetSpriteFrames(gender, charNumber, clothingNumber);
_animatedSprite2D.SpriteFrames = SpriteFrameCreator.Instance.GetSpriteFrames(
gender,
charNumber,
clothingNumber
);

if (IsOnFloor())
{
sprite.Play(JumperAnimations.AnimationIdle);
_animatedSprite2D.Play(JumperAnimations.AnimationIdle);
}
}

Expand All @@ -115,29 +141,21 @@ public void SetGlow()
return;
}

CpuParticles2D particles = GetGlowNode();
Color color = Color.FromHtml(colorString);
color.A = 1f;

particles.SelfModulate = color;
particles.Visible = true;
color.A = 1f;
_cpuParticles2D.SelfModulate = color;
_cpuParticles2D.Visible = true;
}

public void DisableGlow()
{
CpuParticles2D particles = GetGlowNode();

particles.Visible = false;
}

public override void _Ready()
{
GetNode<AnimatedSprite2D>(SpriteNodeName).AnimationFinished += OnSpriteAnimationFinished;
_cpuParticles2D.Visible = false;
}

public void RandomJump()
{
Jump(Rng.IntRange(45, 135), Rng.IntRange(10, 100));
Jump(Rng.IntRange(45, 135), Rng.IntRange(75, 100));
}

public void Jump(int angle, int power)
Expand All @@ -149,11 +167,10 @@ public void Jump(int angle, int power)
_jumpVelocity.X = Mathf.Cos(Mathf.DegToRad(angle + 180));
_jumpVelocity.Y = Mathf.Sin(Mathf.DegToRad(angle + 180));
_jumpVelocity = _jumpVelocity.Normalized() * (float)normalizedPower;

PlayerData.NumJumps++;

_canFadePlayerName = true;
_lastJumpZeroAngle = angle == 90; // 0 in the command is expressed here as 90.

PlayerData.NumJumps++;
}
}

Expand All @@ -165,104 +182,57 @@ public void DisableNameFadeout()
_canFadePlayerName = false;
}

public void SetColor(string hexColor)
public void OnSpriteAnimationFinished()
{
AnimatedSprite2D sprite = GetNode<AnimatedSprite2D>(SpriteNodeName);

sprite.Modulate = Color.FromHtml(hexColor);
sprite.Modulate = new Color(sprite.Modulate.R, sprite.Modulate.G, sprite.Modulate.B, 1f);
if (_animatedSprite2D.Animation == JumperAnimations.AnimationLand)
{
_animatedSprite2D.Play(JumperAnimations.AnimationIdle);
}
}

public override void _PhysicsProcess(double delta)
/// <summary>
/// Resets the alpha component to 1 on player's name label and resets the name fadeout timer.
/// </summary>
public void FlashPlayerName()
{
Vector2 velocity = Velocity;
SetNameAlpha(1f);
ResetNameTimer();
}

private void StopOnFloor()
{
if (IsOnFloor())
{
velocity.Y = 0;
velocity.X = 0;
Velocity = Vector2.Zero;
}
}

// Add the gravity.
private void ApplyInitialGravity(double delta)
{
if (!IsOnFloor())
{
velocity.Y += _gravity * (float)delta;
}

AnimatedSprite2D sprite = GetNode<AnimatedSprite2D>(SpriteNodeName);

if (_jumpVelocity != Vector2.Zero)
{
velocity = _jumpVelocity;

if (!_lastJumpZeroAngle)
{
sprite.FlipH = velocity.X < 0;
}

_jumpVelocity = Vector2.Zero;
}

if (IsOnWall())
{
velocity.X = _previousXVelocity * -0.75f;
Velocity = new(Velocity.X, Velocity.Y + _gravity * (float)delta);
}

Velocity = velocity;

if (Velocity.Y > 0)
{
sprite.Play(JumperAnimations.AnimationJump);
}
else if (Velocity.Y < 0)
{
sprite.Play(JumperAnimations.AnimationFall);
}

bool justLanded = !_wasOnFloor && IsOnFloor();
bool stuckInAir =
(sprite.Animation == JumperAnimations.AnimationFall || sprite.Animation == JumperAnimations.AnimationJump)
&& Velocity.Y == 0;

if (justLanded || stuckInAir)
{
sprite.Play(JumperAnimations.AnimationLand);
}

_wasOnFloor = IsOnFloor();

UpdateNameTransparency();

_previousXVelocity = Velocity.X;

MoveAndSlide();
StorePosition();
}

public void OnSpriteAnimationFinished()
private void ApplyJumpVelocity()
{
AnimatedSprite2D sprite = GetNode<AnimatedSprite2D>(SpriteNodeName);
Velocity = !_jumpVelocity.IsEqualApprox(Vector2.Zero) ? _jumpVelocity : Velocity;

if (sprite.Animation == JumperAnimations.AnimationLand)
// Flip the sprite based on our x velocity, but only if we recently jumped at a non-zero angle
if (!Mathf.IsZeroApprox(Velocity.X) && !_lastJumpZeroAngle)
{
sprite.Play(JumperAnimations.AnimationIdle);
_animatedSprite2D.FlipH = Velocity.X < 0;
}
}

/// <summary>
/// Resets the alpha component to 1 on player's name label and resets the name fadeout timer.
/// </summary>
public void FlashPlayerName()
{
SetNameAlpha(1f);
ResetNameTimer();
// Reset the jump velocity to indicate that we should not continuously apply the calculated velocity
_jumpVelocity = Vector2.Zero;
}

private void ResetNameTimer()
{
_fontVisibilityTimerStartTime = Time.GetTicksMsec();

GetNode<RichTextLabel>(NameNodeName).Visible = true;
_nameLabel.Visible = true;
}

private void UpdateNameTransparency()
Expand All @@ -286,14 +256,47 @@ private float CalculateFontAlpha()
return Math.Max(0, 1 - diff);
}

private CpuParticles2D GetGlowNode()
private void SetNameAlpha(float alpha)
{
return GetNode<CpuParticles2D>(ParticleSystemNodeName);
_nameLabel.Modulate = new Color(1, 1, 1, alpha);
}

private void SetNameAlpha(float alpha)
/// <summary>
/// Causes the character to rotate based on its X velocity, but only at non-zero angles.
/// </summary>
private void RotateInAir(double delta)
{
// We don't want to rotate if we just jumped straight up
if (_lastJumpZeroAngle)
{
// Edge case reset when we jump at the very moment we hit the floor and we keep the previous rotation
_animatedSprite2D.RotationDegrees = 0;

return;
}

// Formula to automatically calculate the rotation speed based on the character's x jump velocity. The maximum
// velocity is 700, but it's a bit too fast, so we want to clamp it at around 600. The formula was shortened and
// tweaked to always output 1 at non-zero angle with low value, with a maximum of 7 at angle of 60, which should
// not increase linearly, but a bit slower at the start. Assuming the full power jump.
// Visualization, caps at J60. Approximately every +100 velocity on the plot is the next 10 angles:
// https://www.wolframalpha.com/input?i=min%28max%28%284sin%28%28abs%28x%29%2F280%29+%2B+300%29%2B5%29%2C1%29%2C+7%29+%3Bx+from+0+to+700
float velocity = Math.Abs(Velocity.X);
float rotationSpeedMultiplier = (float)(4 * Math.Sin((velocity / 280) + 300) + 5);
float clampedMultiplier = Math.Clamp(rotationSpeedMultiplier, 1, 7);

// Calculate the rotation factor and flip the value if we are going left (rotating in the right direction)
float rotationFactor = 200 * (float)delta * (Velocity.X < 0 ? -1 : 1) * clampedMultiplier;

_animatedSprite2D.RotationDegrees = IsOnFloor() ? 0 : _animatedSprite2D.RotationDegrees + rotationFactor;
}

private void BounceOffWall()
{
GetNode<RichTextLabel>(NameNodeName).Modulate = new Color(1, 1, 1, alpha);
if (IsOnWall())
{
Velocity = new(_previousXVelocity * -0.75f, Velocity.Y);
}
}

/// <summary>
Expand Down Expand Up @@ -329,4 +332,27 @@ private void StorePosition()
Position += Vector2.Up * 16;
}
}

private void PlayNotGroundedAnimation()
{
// Going up -> Jump, down -> Fall. Removing the check causes infinite animation start
if (!IsOnFloor())
{
string animation = Velocity.Y < 0 ? JumperAnimations.AnimationJump : JumperAnimations.AnimationFall;

_animatedSprite2D.Play(animation);
}

// Describes a situation when we stopped, but the animation is still playing the Jump/Fall frames
bool hasLandedButStillAnimating =
(
_animatedSprite2D.Animation == JumperAnimations.AnimationFall
|| _animatedSprite2D.Animation == JumperAnimations.AnimationJump
) && Velocity.IsEqualApprox(Vector2.Zero);

if (hasLandedButStillAnimating)
{
_animatedSprite2D.Play(JumperAnimations.AnimationLand);
}
}
}

0 comments on commit 8142ea6

Please sign in to comment.