- Home /
How do you handle Event invocation timing?
Context
I'm making a classic top-down, tile based 2D puzzle game about pushing blocks, similar to Sokoban. Levels are built with GameObjects (as opposed to Tilemaps). Assume tiles are 1 unit big per side.
The Player has a Player
component with various things, but we're focused on this part:
Update() {
change = GetChange();
}
FixedUpdate(){
Vector2 startingPosition = myRigidbody2D.position;
RaycastHit2D hit = Physics2D.Raycast(transform.position, change, 1f);
if(hit.collider.GetComponent<Pushable>()) {
Pushable pushable = hit.collider.GetComponent<Pushable>();
if(pushable.GetDirectionIsAvailable(change))
pushable.Push(change);
}
if(startingPosition != startingPosition + change) {
myRigidbody.position += change;
//Irrelevant code about locking movement so you can't move every single frame.
}
}
Vector3 GetChange() {
// code that returns a Vector3 with magnitude 1 based on player directional input.
}
Blocks have a Pushable
component as follows:
public class Pushable : MonoBehaviour {
public UnityEvent OnWasPushed;
private TileDetector tileDetector => GetComponent<TileDetector>();
OnEnable() {
onWasPushed.AddListener(tileDetector.UpdateTileAtPosition);
}
public void Push(Vector3 direction) {
myRigidbody2D.position += direction;
onWasPushed.Invoke();
}
public bool GetDirectionIsAvailable(Vector3 direction) {
//Code that returns whether there's a free space in that direction
}
}
Since Blocks are to be "destroyed" (their sprite and collider disabled) when touching a "HazardTile" like water, they also have a TileDetector
component:
public class TileDetector : MonoBehaviour
{
public Collider2D tileAtPosition;
/// Returns the first collider at position except itself;
public void UpdateTileAtPosition()
{
Collider2D[] colliders = Physics2D.OverlapPointAll(transform.position);
if(colliders.Length == 0)
return null;
if(colliders[colliders.Length - 1].gameObject == gameObject)
return null;
tileAtPosition = colliders[colliders.Length - 1];
//Code about detecting if the tile is dangerous and "destroying" the object if so.
}
}
In summary, The player pushes the block whenever they move towards it and it has a free space behind it. When this happens, OnWasPushed
is invoked and UpdateTileAtPosition()
in the block's TileDetector
must determine whether a dangerous tile is in this new position and "destroy" the block if necessary.
Problem(s)
The tileAtPosition
variable in TileDetector
is always one "move" behind, which seems to indicate that the Physics2D.Overlap
function checks for colliders before the block is pushed, even though the call was made later (in the same frame).
Here's a capture showing the problem.
This is but one example of a series of situations where an event invocation and a collision check don't follow the expected order.
Things I've tried
I've looked up this issue and the idea of Unity physics (NVidia's PhysX) taking two frames to behave as expected seems to be a common problem developers face when making 2D games, but I haven't been able to get a concrete alternative. People suggest sticking to Physics2D.Overlap
methods instead of Physics2D.OnTrigger
ones but even after the change I'm still facing this issue.
As a temporal solution, I've tried encapsulating some collision-centered functions inside a Coroutine so that the collision check happens one full frame after the movement. This feels unintuitive and hard to keep track of if you're applying it in many different scripts.
This solution also makes it so that objects spend an extra frame in the wrong state. In the case of blocks, they will hover over the "dangerous" tile for one frame before updating. Objects like Lasers which update their start and end coordinates based on event invocations such as these will "stick" to their target for one move after the target has already left the affected tiles. (Lasers are only meant to be shot orthogonally)
Additionally, this single frame yielding method doesn't seem to be compatible with the Command Pattern (unless I'm implementing it wrong) which is very common in turn-based puzzle games for the Undo/Redo functionality.
Additional Comments
Considering the amount of time I've spent looking for a solution to this problem makes me think I might be misunderstanding the nature of events. Is there a standard way of dealing with this? Is a "cheap" solution like manual frame yielding good enough to modify the rest of my logic to accommodate to it? would it make me run into problems down the line?
Answer by sztobar · May 13, 2020 at 03:32 PM
In unity docs of rigidbody it states:
If you change the position of a Rigibody using Rigidbody.position, the transform will be updated after the next physics simulation step
So you're always calling OverlapPointAll
on old position. That's the reason behind your lag. There are a few ways of solving that:
Pass a position change to
onWasPushed
event (so thatPhysics2D.OverlapPointAll
will be called with new position)try to call Physics.SyncTransforms before calling
Physics2D.OverlapPointAll
(I'm not 100% it will work)If you're playing with coroutines, you can check out WaitForFixedUpdate to call a function specifically in the next physics frame, but I doubt it will be necessary, above solutions seems easier to use in your case.
I went for passing a position change to OnWasPushed
and it worked! In case someone else is building a similar game: When implementing the Command Pattern, you can invoke the event directly in your command's Execute()
.