Learning to sail in 12 days to fight pirates, cleanse the sea of toxic ships and to collect as much treasure as possible.
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.
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.