CS 491: Meeting 7 – Smooth Movement and Projectiles
Dear students:
In your prototypes last week, we saw that all of our games are going to involve physical interactions between objects. At the end of our last lab we were manually positioning our players, which meant that we could walk through walls. Also, the movement was jerky. In today’s lab, we look at how we can use the physics engine, smooth out the movement, and launch projectiles from the avatar.
Smoother Motion
This is the controller that we assembed in our last lab exercise.
We assembled the controller as follows:
- Connect 5v on the Arduino to 5v on the joystick.
- Connect ground to ground.
- Connect the horizontal position pin (+VRx) to A0.
- Connect the vertical position pin (+VRy) to A1.
The firmware code we wrote might have looked something like this:
void setup() {
pinMode(A0, INPUT);
pinMode(A1, INPUT);
Serial.begin(9600);
}
void loop() {
int x = analogRead(A0) - 512;
int y = 512 - analogRead(A1);
Serial.print('[');
Serial.print(x);
Serial.print(' ');
Serial.print(y);
Serial.println(']');
delay(50);
}
We connected with the controller in Unity with code that looked something like this in Update
:
string line = serial.ReadLine().TrimEnd();
if (line.Length >= 2 && line[0] == '[' && line[line.Length - 1] == ']') {
string payload = line.Substring(1, line.Length - 2);
string[] tokens = payload.Split(' ');
int x, y;
int.TryParse(tokens[0], out x);
int.TryParse(tokens[1], out y);
if (Mathf.Abs(x) < 10) x = 0;
if (Mathf.Abs(y) < 10) y = 0;
transform.position += new Vector3(x, y, 0) / 1000f;
}
Get your project back to this state if it’s not already there. Comment out OnTriggerEnter2D
.
Rigidbody Physics
Our first step is to let Unity’s physics engine control the position of the player. Physics will do a better job of producing realistic motion than us. Complete these checkpoints:
- On the
Rigidbody2D
component of the player, set Body Type to Dynamic—which means the physics engine will be responsible for positioning the game object. - Test the program. Does the player drop from the sky?
- Set the Gravity Scale to 0 so that the object isn’t pulled downward.
- In your player controller, grab a reference to the
Rigidbody2D
component with code like this:Grabbing a handle to a component of an object like this is a common idiom in Unity development.private new Rigidbody2D rigidbody; void Start() { // ... rigidbody = GetComponent<Rigidbody2D>(); }
- Instead of tweaking the position of the player in
Update
, set the rigidbody’s velocity like this:Vector2 velocity = new Vector2(x, y) / SOME_SCALE_FACTOR; rigidbody.velocity = velocity;
Does this feel any smoother? Probably not. We are still instantaneously setting the velocity, which leads to discontinuities.
Low-pass Filter
We can smooth out the velocity by blending the current velocity with the previous velocity. For example, we might take 30% of the previous velocity and 70% of the current velocity. Complete these checkpoints to enable this smoothing mechanism, which is called a low-pass filter:
- Declare the velocity as instance variable instead of a local variable.
- Initialize the velocity to [0, 0] in
Start
. - In
Update
, compute the velocity as a weighted blend of the previous velocity and the new instantaneous velocity using a weight of your choosing.
When you test the game now, do you see how the movement ramps up and down? For instance, after you let go of the joystick, the player keeps moving for a few frames. But the motion’s still jerky.
Faster Read
To get rid of jerks completely, we need to read faster from the serial port. Let’s switch up our reading algorithm from reading one line at a time to reading as much as data as is available and skipping directly to the most recent [x y]
message. Alter the reading algorithm to this mix of C# and pseudocode:
buffer += serial.ReadExisting() look for last occurrence of ] in buffer if ] appears look for [ that matches ] if [ appears extract out payload between [ and ] // split, parse, and update velocity as before reduce buffer to substring starting after ] else clear buffer since we don't have a complete payload
This algorithm skips over all the old payloads from the controller.
Additionally, tweak the serial port to timeout nearly immediately if there are no bytes available with this code:
void Start() {
// ...
serial.ReadTimeout = 1;
}
When there’s nothing to read, we now get an exception. Use this try-catch in Update
to ignore it:
try {
buffer += serial.ReadExisting();
// ...
} catch (System.TimeoutException) {}
The motion should now be wonderfully smooth.
Projectiles
It’s common to control a player by directly setting the velocity of its Rigidbody2D
. We are, after all, used to players not fully conforming to the laws of physics. But we expect more realistic behavior out of other kinds of objects. Let’s look at how the player can fire off projectiles.
Arduino
We need a fire button. The joystick has a builtin button. Complete these steps to incorporate it into your controller:
- Add a wire from pin 7 on the Arduino to the SW pin on the joystick.
- Set the mode for pin 7 to
INPUT_PULLUP
insetup
. - Read pin 7 using the
digitalRead
method. - Adjust the payload to the following form:
[x y is-button-just-pressed]
. Send a 1 for the third field if and only if the button has just been pressed but it wasn’t pressed before. Send a 0 otherwise.
In a game with rapid fire projectiles, we might want to know if the button is currently down.
Unity
Back in Unity, we respond to a button press by creating a brand new game object and launching it with some force. Complete these steps to the projectiles flying.
- Add a new game object that can act as a projectile. Scale it as desired.
- Give the object a
Rigidbody2D
component. - Ensure that the
Rigidbody2D
component has its Body Type set to Dynamic and its Gravity Scale set to 1. - Drag the object from the Hierarchy into the Project panel. This makes the object a prefab—a prefabricated game object.
- Delete the object from the Hierarchy.
- Add this declaration to your player controller:
public GameObject prefab;
- In
Update
, parse out the third field of the payload. - If this is field is 1, make a new instance of the prefab and grab a reference to its rigidbody with code like this:
GameObject instance = Instantiate(prefab, transform.position, Quaternion.identity); Rigidbody2D thatBody = instance.GetComponent<Rigidbody2D>();
- Instead of setting the projectile’s velocity, apply force to it. Set the push direction to values of your choosing. In your actual games, you probably want to a fire the projectiles off in the direction the player is facing.
thatBody.AddForce(new Vector3(HORIZONTAL_PUSH, VERTICAL_PUSH, 0));
That’s it. You’ve now seen two ways of using rigidbody physics to add some life to your game.
See you next time!