← Back to Portfolio

Toxic Piracy

Learning to sail in 12 days to fight pirates, cleanse the sea of toxic ships and to collect as much treasure as possible.

Engine Unity
Language C#
Timeline 12 Day Game Jam
Status Shipped

Toxic Piracy takes us to the high seas where the player must collect treasure, battle pirates, and cleanse the waters of the patrolling toxic ships. A 3D world with a 2D view of it, helped build a contrast of an arcade feeling with the weight of true nautical battles. With a deadline of 12 days to build from scratch, and The Made To Be Played Jam's central theme of toxic gave me a tight scope and an exact direction. As devs, the concept of our submissions could be however we interpreted. I wanted something that would challenge you to go beyond the toxic danger, something you could replay quickly but still get something new each time.

The challenge was getting the ships to behave the way I was imagining how a real ship would. Taking inspiration from Pirates of the Caribbean, I wanted those big ship battles where the odds looked stacked but there was always a way to fight out of it. I wanted to tackle that replayability and challenge you could get each round from random generation. I needed the ship to feel like it carried its weight in the water, and that if you crashed into an island or rammed an enemy, you'd see that repercussion. For the ships out of my control, it became a matter of figuring out how to know which way to turn, what obstacles to avoid, how to shoot the player, and then get back into the chase.

Core Problem

Delivering that ship feel needed to be real, but how could I do it without taking away the arcade cycle.

Something I discovered with this project which has inspired a new line of thinking for future projects is dot products and contact points. First, using dots to determine where an object is relative to another, or in this case whether the player was closer to an enemy's starboard or port side. Getting the direction to target by subtracting the player's position and the enemy's position, then calculating the broadside directions to see which dot product is closer to 1 helped determine where the least amount of rotation would come from. Upon deciding which side is closer, set the cannon to fire appropriately and call FireCannon() on said cannon to fire. All of this behavior is primarily gated by whether the game is over or not, then whether the enemy still has health left to pursue.

if (!broadsideCalc)
{
	dirToTarget = Vector3.Normalize(player.transform.position - agent.transform.position);
    portCandid = Quaternion.Euler(0, 90f, 0) * dirToTarget;
    starboardCandid = Quaternion.Euler(0, -90f, 0) * dirToTarget;
    portDot = Vector3.Dot(agent.transform.forward, portCandid);
    starbDot = Vector3.Dot(agent.transform.forward, starboardCandid);
    if (portDot > starbDot)
    {
    	targetBroadsideRot = Quaternion.LookRotation(portCandid);
    	broadsideCalc = true;
       	cannonToFire = leftCannon;
    }
    else
    {
    	targetBroadsideRot = Quaternion.LookRotation(starboardCandid);
    	broadsideCalc = true;
    	cannonToFire = rightCannon;
    }
}
				

For the player, getting the main ship to feel like there was weight to the boat and not just that arcade feel, I implemented a drift and bounce float that could either send the player back from hitting an enemy, or a gentle glide in the ocean, bobbing to an eventual stop. For that weight, I designed a ramp and decay system that as the player held onto input, I'd store a max ramp acceleration that increased over time that once the player let go, would decay at a similar rate to let the ship drift. Players would lose that forward drift acceleration, and some health, with an instant collision against an island's kinematic rigidbody.

What worked, and what I'd do differently.

What Worked
  • Cannons firing gated by whether the player can shoot again and with a timed delay to recover.
  • Treasure and Hazard regeneration in the event they spawned in or around an island through collision detection.
  • Forward direction now only happens with direct input. Debugging why steerSpeed was still driving the ship forward or sometimes erratically when idle took an understanding of how the decay system was really meant to work.
What I'd Change
  • Islands would occasionally spawn on top of or in each other, so iterating through a list of stored spawned vectors could help prevent that.
  • Prevent nearby spawning of enemies, islands, treasures and hazards within player view or on an area too close to the player.
  • Generate new and bigger pirates that would vary in health and fire rate to further improve on different routines of gameplay.