teaching machines

CS 491: Lecture 4 – Pathlete

February 26, 2018 by . Filed under gamedev, lectures, spring 2018.

Dear students:

Today we implement a little puzzle game inspired by The Legend of Zelda: Link’s Awakening:

To make this happen, we’ll use an analog joystick to direct the platform. These commonly have five pins: voltage, ground, a horizontal analog pin, a vertical analog pin, and a digital button pin. Let’s hook up circuit like so:

I’m going to ditch the breadboard today. Joysticks and breadboards don’t go well together. The joystick we’ve got today has male pins, so we can use male-female wires to plug the joystick in directly to the Arduino.

Now we want to write a sketch for communicating the joystick state over the serial port. I’ll get us started:

#include "Arduino.h"

int old_x = 0;
int old_y = 0;

void setup() {
  pinMode(A0, INPUT);
  pinMode(A1, INPUT);
  Serial.begin(9600);
}

void loop() {
  int x = analogRead(A0);
  int y = analogRead(A1);

  Serial.println(x);
  Serial.println(y);
  Serial.println();

  old_x = x;
  old_y = y;
}

We don’t really want an analog device, however. We just care about the direction. How might we “digitalize” our signal?

Also, we only want to send a signal up when there’s a change. Shifting the joystick to a neutral position doesn’t count as a change. This is the solution I ended up with:

#include "Arduino.h"

int old_dx = 0;
int old_dy = 0;

void setup() {
  pinMode(A0, INPUT);
  pinMode(A1, INPUT);
  Serial.begin(9600);
}

int sign(int value) {
  if (value > 20) {
    return 1;
  } else if (value < -20) {
    return -1;
  } else {
    return 0;
  }
}

void loop() {
  int dx = analogRead(A0) - 512;
  int dy = analogRead(A1) - 512;

  if (abs(dx) > abs(dy)) {
    dy = 0;
    dx = sign(dx);
  } else {
    dx = 0;
    dy = sign(dy);
  }

  if ((dx != 0 || dy != 0) &&
      (old_dx != dx || old_dy != dy)) {
    Serial.write(dx + 1);
    Serial.write(dy + 1);
    old_dx = dx;
    old_dy = dy;
  }
}

Now, to Unity. We’ll eventually lay out a level based on a plain text file, but for today, let’s just get the platform moving. We’ll add a square sprite representing out platform. And here’s our starter code for serial reading:

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO.Ports;
using UnityEngine;

public class GameController : MonoBehaviour {
  private SerialPort serial;

  void Start() {
    serial = new SerialPort("/dev/tty.usbmodem1411", 9600);
    serial.Open();
  }

  void Update() {
  }
}

In Update, we’ll need to detect a move on the joystick. If it’s the first move, we’ll want to kick off the sliding in the push direction. If it’s not the first move, we just need to set the direction that the platform will go next. Perhaps something like this:

while there are two bytes to read
  deltaX = read
  deltaY = read
  nudge if we haven't before

For the nudging, we want the platform to slide smoothly from its current location to the tile in the push direction. For this we’ll use what Unity calls a coroutine, which you can think of as a side process that runs along with your game’s normal flow. It’s really handy for orchestrating animations without muddying up the Update method. Here’s how we kick off a coroutine that we’ve named Nudge if it isn’t already running:

if (nudger == null) {
  nudger = StartCoroutine(Nudge());
}

To define Nudge, we say:

private IEnumerator NudgeAlong() {
  while (!isDoneAnimating) {
    // Do animation step
    ...

    // We can either wait a single frame
    yield return null;

    // Or a particular amount of time
    yield return new WaitForSeconds(0.1f);
  }
}

I follow a very particular pattern for scheduling animations that has suited me well. Here it is in pseudocode:

startProperty = ...
stopProperty = ...

startTime = currentTime
targetDuration = ...
elapsed = 0

while elapsed < targetDuration
  proportion = elapsed / targetDuration
  currentProperty = interpolate(startProperty, stopProperty, proportion)
  yield
  elapsed = currentTime - startTime

// Because we might not stop exactly at stopTime
currentProperty = stopProperty

For shifting the platform, we can do this:

Vector3 startPosition = platform.transform.position;
Vector3 endPosition = startPosition + Vector3(deltaX, deltaY, 0);

float targetDuration = 1.5f;
float startTime = Time.time;
float elapsed = 0.0f;

while (elapsed <= targetDuration) {
  float proportion = elapsed / targetDuration;
  platform.transform.position = Vector3.Lerp(startPosition, endPosition, proportion);
  yield return null;
  elapsed = Time.time - startTime;
}

platform.transform.position = endPosition;

See you next time!

Sincerely,