5.4 KiB
title | date | weight |
---|---|---|
Ball Respawn | 2019-06-04T10:44:53-07:00 | 20 |
Right now the ball respawns using fixed magic values. That's pretty boring and bad!
When the ball is served in Pong, it fires from a random position along the center line at a random angle (with some constraints). Sound's like we're gonna need some vector math.
We haven't talked about vectors much yet, but to recap, a 2D vector is simply a mathematical structure composed of an x and y component. There are various clever mathematical operations we can do on vectors that make them very useful for games. C# provides us with a useful System.Numerics library that contains vector math structures; right now we use Vector2 to represent velocity.
Let's break down some of the math here.
We want the ball to fire in a random direction, but we don't want its trajectory to be too vertical, or it will take forever to get to one of the paddles, and it will also be very hard to hit. We also want it to travel at the same speed regardless of its direction.
Let's start with a vector with an x component of our desired speed and a y component of 0. You can think of this as an arrow pointing directly to the right. Now imagine that arrow as the hand of a clock. How can we describe the angle of the clock hand? As the hand rotates around it differs from its original orientation in an amount of units called radians. When that angle changes by 2 times pi, it ends up in the same position. So a rotation a quarter of the way around the clock would be pi divided by 2.
You can see that if we rotate our direction vector by pi/2 radians, it will face straight down. Now, what we want is for the ball to be served at angles like this:
The non-shaded area represents the angles that we want the ball to be served at. What angle is that exactly?
Well, a lot of what we do in game math is guesstimation. "Close enough" can be a powerful phrase! We can always easily tweak the exact values later if we architect our game properly.
If we draw it out, we know that a quarter-circle rotation is pi/2 radians. The angle of our serve range seems to be roughly half that. So our rotation would be pi/4 radians. Sounds reasonable as a starting angle to me. How do we actually represent this range?
What if the rotation is negative? Well, our positive rotations have been going clockwise - so negative rotations go counter-clockwise! That means our possible serve angle is somewhere between -pi/4 and pi/4.
So now we need to actually pick a random rotation within this range. How should we do that? C# doesn't have anything built-in for this so I usually write a helper, since it's so common to want a random real number in a certain range.
Let's create PongFE/Utility/MathHelper.cs:
using System;
public static class MathHelper
{
private readonly static Random s_random = new Random();
public static double RandomDouble(double min, double max)
{
return (s_random.NextDouble() * (max - min)) + min;
}
}
Random.NextDouble() returns a random real number between 0 and 1. So our RandomDouble function will return a random real number between low and high.
While we're at it, let's add a few more math helper functions.
public static int Dice(int n)
{
return (int)Math.Floor(RandomDouble(0, n));
}
public static bool CoinFlip()
{
return Dice(2) == 0;
}
We need one last utility function. Bizarrely, Vector2 does not provide a built-in rotation function. Let's fix that.
In Vector2Extensions.cs:
public static Vector2 Rotate(this Vector2 vector, float radians)
{
return Vector2.Transform(vector, Matrix3x2.CreateRotation(radians));
}
Now we can construct a formula for our random serve direction. First, let's change the BallSpawner to take a speed value instead of a specific velocity.
{{% notice tip %}} The difference between speed and velocity is that velocity contains directional information, while speed does not. {{% /notice %}}
BallSpawnMessage.cs:
public float Speed { get; }
...
public BallSpawnMessage(Position2D position, float speed, int width, int height)
{
Position = position;
Speed = speed;
Width = width;
Height = height;
}
Now, in BallSpawner.cs:
var direction = MathHelper.RandomDouble(-System.Math.PI / 4.0, System.Math.PI / 4.0);
var velocity = new Vector2(message.Speed * (MathHelper.CoinFlip() ? -1 : 1), 0).Rotate((float)direction);
AddComponent(ball, new VelocityComponent(velocity));
We need the ball to be able to be served left or right. To make our direction point left, we can make the x component of the vector negative. So the above formula gives a 50% chance of serving left or right.
This maybe isn't the best place for this logic, but since right now we always want the ball to be served randomly, it's probably fine to have this logic in the BallSpawner. Sometimes it isn't the best use of time to write the most abstract-possible system all up front if you're pretty sure you aren't gonna need it. You will have to use your judgment to make these kinds of calls.
The moment of truth... Run the game.
You should see that the ball gets served at random angles. The angle is probably a bit generous as it is but we can leave it for now.