> Summary_
Dungeonball: The Slimekeep is a physics-based FPS where you fight your way through a horde of slimes. Pick up dodgeballs in both hands and launch them to defeat enemies.
As part of a Master's assignment, I had to recreate one of Sokpop's games. The task involved analysing the original game's mechanics and re-implementing them from scratch, with the restriction of using only primitive shapes for visuals.
I focused on maximising game feel by making every action satisfying. A key part of this was making the act of picking up and throwing dodgeballs enjoyable. Balls launch at high speed but retain a floaty feel, allowing players to catch them again midair.
> Pickup Mechanic_
The game's fast pace meant picking up dodgeballs needed to be both quick and accurate. Players can collect balls within a given range in front of them without needing to look directly at the object. However, to maintain accuracy, any balls the player is directly looking at are prioritised over others nearby.
To achieve this, I used two physics casts: every frame, a SphereCast is performed in front of the player and the first object hit is stored.
public void ScanForGrabbables(Vector3 aimOrigin, Vector3 aimDirection)
{
_scannedGrabbable = null;
if (Physics.SphereCast(aimOrigin, _grabSphereRadius, aimDirection,
out RaycastHit hit, _grabRange, _grabbableLayer))
{
_scannedGrabbable =
hit.collider.gameObject.GetComponent<Grabbable>();
}
} Debug visuals showing the SphereCast detecting grabbable objects.
The second cast is made only when the player presses the fire button. A RayCast looking directly in front of the player is performed. If a ball is found, it is picked up. If not, the previously stored object from the SphereCast is used instead.
public bool TryGrabGrabbable(Vector3 aimOrigin, Vector3 aimDirection,
out Grabbable grabbable)
{
grabbable = null;
// get object directly in front of player if available
if (TryGetLineOfSightObject(aimOrigin, aimDirection, ref grabbable))
return true;
// if not, use the object stored from the last scan
if (_scannedGrabbable != null)
{
grabbable = _scannedGrabbable;
return true;
}
// if no object in range, return false
return false;
}
private bool TryGetLineOfSightObject(Vector3 aimOrigin,
Vector3 aimDirection,
ref Grabbable grabbable)
{
if (Physics.Raycast(aimOrigin, aimDirection, out RaycastHit hit,
_grabRange, _grabbableLayer))
{
grabbable = hit.collider.gameObject.GetComponent<Grabbable>();
}
return grabbable != null;
} Note the red line turning green when the player looks directly at a ball.
It's important to note why the SphereCast is run every frame rather than only when the player presses the fire button. The crosshair UI needs to update each frame to indicate to the player whether they are within range of a dodgeball. This real-time feedback is essential for accuracy in gameplay, which is why the cast is performed continuously.
/// <summary>
/// Checks if player has changed from "hovering over target" to
/// "not hovering over target" or vice versa.
/// Invokes event if change is detected.
/// </summary>
private void CheckScannedGrabbablesHasChanged()
{
// Is player hovering over a target this frame?
bool foundGrabbableThisFrame = _grabber.ScannedGrabbable != null;
// If the state has changed since the last scan, invoke event
// (Crosshair UI will subscribe to this event and update accordingly)
if (_lastScanFoundGrabbable != foundGrabbableThisFrame)
OnScannedForGrabbablesChange?.Invoke(foundGrabbableThisFrame);
// Update state for next frame
_lastScanFoundGrabbable = foundGrabbableThisFrame;
} Note how the player picks up a different ball depending on where they're looking.