Skip to content

Commit

Permalink
Refactor & fix bugs.
Browse files Browse the repository at this point in the history
Player:

  - Fix some player state machine bugs preventing player from transitioning to
    the correct state due to having multiple valid triggers, which in turn is
    caused by not being specific enough when specifying trigger conditions
    during state machine initialization. This reduces strange bugs where the
    player gets stuck in a specific state or won't take a valid action like
    reading a sign, or the player doesn't respond to a specific valid input.

  - Remove player idle back animation delay when finished reading a sign, at
    least for now. It looked a bit awkward when finished reading a sign from too
    far off center.

Editor / Scenes:

  - Simplify main scene by combining upper & lower cliffs to reduce level
    complexity and duplication by almost 50%. Merge lower cliffs into upper
    cliffs and rename upper cliffs to cliffs.

  - Restructure the ground colliders to make them more reliable & way less
    buggy.

  - Reorganize & rename many nodes in Godot editor for simplicity & readability,
    which was made possible by the merging of upper and lower cliffs.

  - Reorganize the 10 waterfall sections so that section 1 is the very top and
    section 10 is the very bottom, respectively, which is a much more intuitive
    layout.

  - Lock some nodes in the editor to prevent them from being accidentally
    modified.

Tiles:

  - Heavily rework tile intersection code in Tools.cs to improve accuracy and
    eliminate bugs. Allow units per tile aka cell size to vary from 1 to 16 for
    precise placement of tiles without affecting collision and overlap
    detection.

  - Greatly simplify player updates in Cliff.cs for detecting both cliff
    enclosing & ice intersection code.

Signals:

  - Move ground detection signal callbacks for the player from Cliffs.cs to
    Player.cs which greatly simplifies everything, including not having to add
    duplicate callbacks in both classes.

  - Block more signals while changing player's animation-based collision shape
    to prevent signal callbacks from being triggered more than they should be.
    This is an ongoing workaround for
    godotengine/godot#14578 "Changing node parent
    produces Area2D/3D signal duplicates".

  - Improve naming of some signal callback methods.

Player.cs refactoring:

  - Remove the use of delta from Player.cs, where it mainly a hack; awaiting a
    certain amount of idle frames has been more efficient.

  - Add more private convenience methods for energy meter management to simplify
    the code & increase readability.

  - Add more private convenience methods for sign reading management to simplify
    the code & increase readability.

General refactoring:

  - Move more GetNode calls into _Ready methods and assign them to fields for
    efficiency, code simplicity, readability, & less possibilities for bugs.

Tools:

  - Don't throw an exception in Tools when checking if any arrow key is pressed
    except the specified one, when the specified input is invalid; just return
    false to decrease game crashes.

Extensions:

  - Add a string extension method for allowing the use of the null coalescing
    operator to work with empty strings, which greatly simplifies
    string-handling code, e.g., when using the ternary operator to return
    strings from methods.

Cleanup:

  - Remove unused & obsolete classes.
  - Remove unused & obsolete methods.
  - Remove outdated & unnecessary comments.
  - Suppress unnecessary compiler warnings with comments for increased readability.
  - Remove obsolete compiler warning suppression comments.
  - Rename some outdated variables for increased clarity on their purpose.
  - Rename some methods for simplicity.
  - Reorganize order of some fields for increased readability>
  - Remove obsolete formatter-disabling tags for code blocks.
  - Add formatter-disabling tags where necessary.
  - Add some TODO's.
  - Improve many log messages.
  - Improve code formatting.
  • Loading branch information
knightofiam committed Sep 15, 2021
1 parent cf4e8af commit 48d5b9c
Show file tree
Hide file tree
Showing 14 changed files with 816 additions and 909 deletions.
1 change: 1 addition & 0 deletions AbstractPerch.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ public Vector2 RandomPoint (RandomNumberGenerator rng, GetGlobalScale getGlobalS
public bool Contains (Vector2 globalPosition) => _perchableAreasInGlobalSpace.Any (x => AlmostHasPoint (x, globalPosition, _positionEpsilon));
public virtual bool HasParentName (string name) => false;
public bool Is (string name, Vector2 globalOrigin) => Name == name && AreAlmostEqual (GlobalOrigin, globalOrigin, _positionEpsilon);
// ReSharper disable once MemberCanBePrivate.Global
public bool Equals (IPerchable other) => !ReferenceEquals (null, other) && (ReferenceEquals (this, other) || Is (other.Name, other.GlobalOrigin));
public override bool Equals (object obj) => !ReferenceEquals (null, obj) && (ReferenceEquals (this, obj) || obj.GetType() == GetType() && Equals ((IPerchable)obj));
// @formatter:on
Expand Down
69 changes: 41 additions & 28 deletions Butterfly.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

// TODO Adjust idle position to synchronize with sprite animation for KinematicPerch.
// TODO Use nonlinear interpolation to accelerate / decelerate.
// TODO Test without custom position epsilon.

public class Butterfly : AnimatedSprite
{
Expand Down Expand Up @@ -126,16 +127,18 @@ public bool DrawPerchPointFilled
private DrawRect _drawRect;
private IPerchable _perch;
private Vector2 _perchPoint;
private Node _perchingBody;
private Node _perchableNode;
private Area2D _perchingColliderArea;
private CollisionShape2D _perchingCollider;
private float _maxOscillationAngleVariation;
private readonly RandomNumberGenerator _rng = new();
private readonly List <Vector2> _path = new();
private readonly float _ninety = Mathf.Deg2Rad (90);
private readonly List <IPerchable> _perches = new();
private readonly List <IPerchable> _perchesUnvisited = new();
private readonly List <IPerchable> _unperchablePerches = new();
private readonly List <Node2D> _perchableNodes = new();
private readonly PerchableDrawPrefs _perchableDrawPrefs = new();
private readonly float _ninety = Mathf.Deg2Rad (90);
private Log _log;

private enum State
Expand All @@ -160,6 +163,8 @@ public override void _Ready()
_log = new Log (Name) { CurrentLevel = LogLevel };
_rng.Randomize();
Animation = FlyingAnimation;
_perchingColliderArea = GetNode <Area2D> ("PerchingCollider");
_perchingCollider = _perchingColliderArea.GetNode <CollisionShape2D> ("CollisionShape2D");
_maxOscillationAngleVariation = Mathf.Deg2Rad (MaxOscillationAngleVariationDegrees);
_drawPrimitive = delegate (Vector2[] points, Color[] colors, Vector2[] uvs) { DrawPrimitive (points, colors, uvs); };
_drawRect = delegate (Rect2 rect, Color color, bool filled) { DrawRect (rect, color, filled); };
Expand Down Expand Up @@ -199,13 +204,11 @@ public override void _Process (float delta)
perch.GlobalOrigin = node.GlobalPosition;
}

// @formatter:off
// x & y are intentionally reversed here, see: https://github.com/godotengine/godot/issues/17405
if (Mathf.Sign (((Node2D)sprite.GetParent()).Scale.y) == -1 && !perch.FlippedHorizontally) perch.FlipHorizontally();
if (Mathf.Sign (((Node2D)sprite.GetParent()).Scale.x) == -1 && !perch.FlippedVertically) perch.FlipVertically();
if (Mathf.Sign (((Node2D)sprite.GetParent()).Scale.y) == 1 && perch.FlippedHorizontally) perch.FlipHorizontally();
if (Mathf.Sign (((Node2D)sprite.GetParent()).Scale.x) == 1 && perch.FlippedVertically) perch.FlipVertically();
// @formatter:on

if (!perch.Disabled) continue;

Expand Down Expand Up @@ -249,40 +252,40 @@ public override void _Draw()
}

// ReSharper disable once UnusedMember.Global
public void _OnPerchingBodyColliderEntered (Node body)
public void _OnPerchingColliderAreaEntered (Area2D area)
{
if (!body.IsInGroup ("Perchable")) return;
if (area.GetParent() is not AnimatedSprite sprite || !sprite.IsInGroup ("Perchable")) return;

_log.All ($"Entering body perch: {NameOf (body, Position)}.");
_perchingBody = body;
_log.All ($"Entering animated sprite perch: {NameOf (sprite)}.");
_perchableNode = sprite;
}

// ReSharper disable once UnusedMember.Global
public void _OnPerchingAreaColliderEntered (Area2D area)
public void _OnPerchingColliderBodyEntered (Node body)
{
if (!area.IsInGroup ("Player") || area.GetParent() is not AnimatedSprite sprite) return;
if (body is not TileMap tileMap || !tileMap.IsInGroup ("Perchable")) return;

_log.All ($"Entering area perch: {NameOf (sprite, Position)}.");
_perchingBody = sprite;
_log.All ($"Entering tile perch: {NameOf (tileMap)}.");
_perchableNode = tileMap;
}

// @formatter:off

// ReSharper disable once UnusedMember.Global
public void _OnObstacleColliderEntered (Node body)
public void _OnObstacleColliderBodyEntered (Node body)
{
if (body is StaticBody2D || body.IsInGroup ("Perchable") || body.IsInGroup ("Perchable Parent") || body.IsInGroup ("Ground")) return;

_log.Debug ($"Encountered obstacle: {NameOf (body, Position)}.");
_log.Debug ($"Encountered obstacle: {NameOf (body)}");
_stateMachine.To (State.Evading);
}

// ReSharper disable once UnusedMember.Global
public void _OnObstacleColliderExited (Node body)
public void _OnObstacleColliderBodyExited (Node body)
{
if (body is StaticBody2D || body.IsInGroup ("Perchable") || body.IsInGroup ("Perchable Parent") || body.IsInGroup ("Ground")) return;

_log.Debug ($"Evaded obstacle: {NameOf (body, Position)}.");
_log.Debug ($"Evaded obstacle: {NameOf (body)}.");
_stateMachine.To (State.Flying);
}

Expand Down Expand Up @@ -310,8 +313,8 @@ private void Perch (float delta)
{
if (!_perch.Contains (Position) || !AreAlmostEqual (Position, _perchPoint, PositionEpsilon))
{
_log.All (
$"Found correct perch: {NameOf (_perchingBody, Position)}. Continuing perching along same path from position {Position} to reach perch point {_perchPoint}.");
_log.All ($"Found correct perch: {NameOf (_perchableNode)}." +
$"Continuing perching along same path from position {Position} to reach perch point {_perchPoint}.");

Move (PerchingSpeed, delta);

Expand Down Expand Up @@ -352,37 +355,35 @@ private void Move (float speed, float delta)

private bool ArrivedAtPerch()
{
if (_perchingBody == null) return false;
if (_perchableNode == null) return false;

IPerchable perch = null;
var position = Vector2.Zero;
var cell = Vector2.Zero;

switch (_perchingBody)
switch (_perchableNode)
{
case TileMap tileMap:
{
cell = GetIntersectingTileCell (Position, tileMap);
position = GetIntersectingTileCellGlobalPosition (Position, tileMap);
perch = _perchesUnvisited.FirstOrDefault (x => x.Is (GetIntersectingTileName (Position, tileMap), position));
position = GetIntersectingTileCellGlobalPosition (_perchingColliderArea, _perchingCollider, tileMap);
perch = _perchesUnvisited.FirstOrDefault (x => x.Is (NameOf (tileMap), position));

break;
}
case AnimatedSprite sprite:
{
position = sprite.GlobalPosition;
perch = _perchesUnvisited.FirstOrDefault (x => x.Is (sprite.Animation, position));
perch = _perchesUnvisited.FirstOrDefault (x => x.Is (NameOf (sprite), position));

break;
}
}

if (_perch.Equals (perch)) return true;

_log.All ($"Found wrong perch: {NameOf (_perchingBody, Position)} at position {position}, cell {cell}.");
_log.All ($"_perch: {_perch}, perch: {perch}, _perchingBody: {_perchingBody}");
_log.All ($"Found wrong perch: {NameOf (_perchableNode)} at position {position}.");
_log.All ($"_perch: {_perch}, perch: {perch}, _perchingBody: {_perchableNode}");
_log.Debug ($"Continuing flying along same path toward correct perch:\n{_perch}.");
_perchingBody = null;
_perchableNode = null;

return false;
}
Expand Down Expand Up @@ -479,4 +480,16 @@ private void ChangeDestination()
_perchesUnvisited.Remove (_perch);
StartMoving();
}

// @formatter:off

private string NameOf (Node node) =>
node switch
{
TileMap tileMap => GetIntersectingTileName (_perchingColliderArea, _perchingCollider, tileMap).NullIfEmpty() ?? node.Name,
AnimatedSprite sprite => sprite.Animation,
_ => node.Name
};

// @formatter:on
}
69 changes: 10 additions & 59 deletions Cliffs.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Godot;
using static Tools;

Expand All @@ -21,7 +22,6 @@ public enum Season
private AnimatedSprite _playerSprite;
private Area2D _playerArea;
private Rect2 _playerRect;
private Vector2 _playerExtents;
private Vector2 _playerPosition;
private string _playerAnimation;
private CollisionShape2D _playerAnimationCollider;
Expand All @@ -30,10 +30,6 @@ public enum Season
private AudioStreamPlayer _ambiencePlayer;
private AudioStreamPlayer _musicPlayer;
private TileMap _iceTileMap;
private Vector2 _topLeft;
private Vector2 _bottomRight;
private Vector2 _topRight;
private Vector2 _bottomLeft;
private readonly Dictionary <Season, int> _waterfallZIndex = new();
private readonly Dictionary <Season, AudioStream> _music = new();
private readonly Dictionary <Season, AudioStream> _ambience = new();
Expand Down Expand Up @@ -69,19 +65,12 @@ public override void _Ready()
_ambiencePlayer = GetNode <AudioStreamPlayer> ("../AmbiencePlayer");
_musicPlayer = GetNode <AudioStreamPlayer> ("../MusicPlayer");
_iceTileMap = GetNode <TileMap> ("Ice");
_colliders.Add (GetNode <CollisionShape2D> ("Extents 1"));
for (var i = 1; i <= 5; ++i) _colliders.Add (GetNode <CollisionShape2D> ("Extents " + i));
_player = GetNode <Player> ("../Player");
_playerSprite = _player.GetNode <AnimatedSprite> ("AnimatedSprite");
_playerArea = _playerSprite.GetNode <Area2D> ("Area2D");
_playerAnimation = _playerSprite.Animation;
_playerAnimationCollider = _playerArea.GetNode <CollisionShape2D> (_playerAnimation);

if (Name == "Upper Cliffs")
{
_colliders.Add (GetNode <CollisionShape2D> ("Extents 2"));
_colliders.Add (GetNode <CollisionShape2D> ("Extents 3"));
}

InitializeSeasons();
}

Expand All @@ -95,15 +84,15 @@ public override void _Process (float delta)
public override void _UnhandledInput (InputEvent @event)
{
if (IsReleased (Tools.Input.Season, @event) && !_seasonChangeInProgress) NextSeason();
if (IsReleased (Tools.Input.Music, @event) && Name == "Upper Cliffs") ToggleMusic();
if (IsReleased (Tools.Input.Music, @event)) ToggleMusic();
}

// ReSharper disable once UnusedMember.Global
public void _OnWaterfallEntered (Area2D area)
{
if (!area.IsInGroup ("Player")) return;

_log.Info ($"{area.GetParent().Name} entered waterfall.");
_log.Info ($"Player entered waterfall.");
_isPlayerInWaterfall = true;
_player.IsInFrozenWaterfall = CurrentSeason == Season.Winter;
}
Expand All @@ -113,7 +102,7 @@ public void _OnWaterfallExited (Area2D area)
{
if (!area.IsInGroup ("Player")) return;

_log.Info ($"{area.GetParent().Name} exited waterfall.");
_log.Info ($"Player exited waterfall.");
_isPlayerInWaterfall = false;
_player.IsInFrozenWaterfall = false;
}
Expand All @@ -137,24 +126,6 @@ public void _OnCliffsExited (Area2D area)
_log.Debug ($"Player exited {Name}.");
}

// ReSharper disable once UnusedMember.Global
public void _OnUpperCliffsGroundEntered (Area2D area)
{
if (!area.IsInGroup ("Player") || Name != "Upper Cliffs") return;

_log.Debug ($"Player entered ground of {Name}.");
_player.IsInGround = true;
}

// ReSharper disable once UnusedMember.Global
public void _OnUpperCliffsGroundExited (Area2D area)
{
if (!area.IsInGroup ("Player") || Name != "Upper Cliffs") return;

_log.Debug ($"Player exited ground of {Name}.");
_player.IsInGround = false;
}

private void InitializeSeasons()
{
foreach (Season season in Enum.GetValues (typeof (Season)))
Expand All @@ -180,8 +151,6 @@ private void ChangeSeasonTo (Season season)

private void UpdateWaterfall (Season season, float delta)
{
if (Name != "Upper Cliffs") return;

var isWinter = season == Season.Winter;
var waterfall = GetNode <Area2D> ("Waterfall");
waterfall.Visible = true;
Expand Down Expand Up @@ -212,8 +181,8 @@ private async void UpdateFrozenWaterfallTopGround (PhysicsBody2D waterSurfaceCol
var isWinter = season == Season.Winter;
waterSurfaceCollider.SetCollisionMaskBit (0, isWinter);
waterSurfaceCollider.SetCollisionLayerBit (1, isWinter);
GetNode <CollisionShape2D> ("../Upper Cliffs/Upper Cliffs Top Ground Static Collider 3/CollisionShape2D").Disabled = isWinter;
GetNode <CollisionShape2D> ("../Upper Cliffs/Cliff Edge 3/CollisionShape2D").Disabled = isWinter;
GetNode <CollisionShape2D> ("Ground 3/CollisionShape2D").Disabled = isWinter;
GetNode <CollisionShape2D> ("Edge 3/CollisionShape2D").Disabled = isWinter;
await ToSignal (GetTree().CreateTimer (delta, false), "timeout");

foreach (int shapeOwnerId in waterSurfaceCollider.GetShapeOwners())
Expand Down Expand Up @@ -292,31 +261,13 @@ private void UpdatePlayer()
AreAlmostEqual (_playerAnimationCollider.GlobalPosition, _playerPosition, 0.001f)) return;

_playerAnimation = _playerSprite.Animation;
_playerExtents = GetExtents (_playerArea, _playerAnimation);
_playerAnimationCollider = _playerArea.GetNode <CollisionShape2D> (_playerAnimation);
_playerPosition = _playerAnimationCollider.GlobalPosition;
_playerRect.Position = _playerPosition - _playerExtents;
_playerRect.Size = _playerExtents * 2;
_playerRect = GetAreaColliderRect (_playerArea, _playerAnimationCollider);
_cliffRects.Clear();

foreach (var collider in _colliders)
{
var extents = (collider.Shape as RectangleShape2D)?.Extents ?? Vector2.Zero;
_cliffRects.Add (new Rect2 (collider.GlobalPosition - extents, extents * 2));
}

_cliffRects.AddRange (_colliders.Select (x => GetAreaColliderRect (this, x)));
_player.IsInCliffs = _isPlayerIntersectingCliffs && IsEnclosedBy (_playerRect, _cliffRects);
_topLeft = _playerArea.GlobalPosition - _playerExtents;
_bottomRight = _playerArea.GlobalPosition + _playerExtents;
_topRight.x = _playerArea.GlobalPosition.x + _playerExtents.x;
_topRight.y = _playerArea.GlobalPosition.y - _playerExtents.y;
_bottomLeft.x = _playerArea.GlobalPosition.x - _playerExtents.x;
_bottomLeft.y = _playerArea.GlobalPosition.y + _playerExtents.y;

_player.IsTouchingCliffIce = _iceTileMap.Visible && (IsIntersectingAnyTile (_topLeft, _iceTileMap) ||
IsIntersectingAnyTile (_bottomRight, _iceTileMap) ||
IsIntersectingAnyTile (_topRight, _iceTileMap) ||
IsIntersectingAnyTile (_bottomLeft, _iceTileMap));
_player.IsTouchingCliffIce = _iceTileMap.Visible && IsIntersectingAnyTile (_playerArea, _playerAnimationCollider, _iceTileMap);
}

private void ToggleMusic() => _musicPlayer.Playing = !_musicPlayer.Playing;
Expand Down
Loading

0 comments on commit 48d5b9c

Please sign in to comment.