Making a Game in 2 Weeks: Day 4

Day 4 of my Game in Two Weeks brings the next 3 lines on the schedule. Simple flight controls, a simple race course, and a simple self-driving AI. I estimated them at slated for 3 hours, 3 hours, and 2 hours respectively.

Day 4 part 1. Simple Controls

Making an intuitive control system can be tricky. It needs to be close to what other games use so that players can jump right into the game and start playing. It needs to have a good feel, responding appropriately in a game-specific way, so the controls feel distinctive and fun.

At this point I’m looking at a general input system that can be tuned later. I have decided on a 2-joystick style control system. For a phone or tablet, a virtual joystick is created when you place your thumb down on either the right or left side of the device, and the joystick is moved as you slide around relative to the start position. The left joystick will control a left/right turn and acceleration. The right joystick will control height, and tapping on the right joystick will activate the kart’s current power up.

Since my desktop computer doesn’t have a touch screen, I’m doing development using an xbox USB controller. That controller conveniently has two analog joysticks that feel similar to the handheld’s two virtual joysticks. The handheld’s right joystick tap can be approximated by tapping the right joystick button, which on the controller is treated as a button click.

Hooking up the joystick in Unity is not too difficult. When an xbox controller is plugged in, it appears to the system as a ten-button, seven-axis controller. I needed to tell Unity which of the sticks and buttons to use. By default the left stick is hooked up as a two-axis controller. The left stick x-axis is ‘horizontal’, the y-axis is ‘vertical’. To hook up the right joystick, I opened up Unity’s input device list, created an axis called RightX hooked up to the right stick’s X axis (axis 4) and RightY hooked up to the Y axis (axis 5). I also created a new button input for the right stick press (button 10).

For completeness sake, I also created keyboard button shortcuts. Unity already has a WASD configuration on the horizontal and vertical axis. I created a second mapping for IJKL for the right stick, and U for the button.

With all that in place, I began writing my script. The virtual joystick controller is already present in Unity’s Mobile package, so I use those directly. Since I don’t know up front which stick they’ll be using, I pulled the values out like so:

var leftX :float = 0;
var leftY :float = 0;
var rightX :float  = 0;
var rightY :float  = 0;

// Default is to use the touchpads
if(moveTouchPad != null && rotateTouchPad != null)
{
    leftX = moveTouchPad.position.x;
    leftY = moveTouchPad.position.y;
    rightX = rotateTouchPad.position.x;
    rightY = rotateTouchPad.position.y;
}

// Detect a keyboard or joystick alternate control
var j1Vertical :float = Input.GetAxisRaw("Vertical");
var j1Horizontal :float = Input.GetAxisRaw("Horizontal");
if(Mathf.Abs(j1Vertical) > 0.1f)
    leftY = j1Vertical;
if(Mathf.Abs(j1Horizontal) > 0.1f)
    leftX = j1Horizontal;
var j2Vertical :float = Input.GetAxisRaw("RightY");
var j2Horizontal :float = Input.GetAxisRaw("RightX");
if(Mathf.Abs(j2Vertical) > 0.1f)
    rightY = j2Vertical;
if(Mathf.Abs(j2Horizontal) > 0.1f)
    rightX = j2Horizontal;

That code block gives me a left and right stick that is consistent on the mobile device and on the PC. That way I can use Unity’s playback feature without needing to dump it to the device every time. At that point I can just use this block of code to move the character along the directions I want.

// Apply movement
var movement :Vector3 = thisTransform.TransformDirection( Vector3( leftX, 0, leftY ) );
movement.Normalize();

// Forward/backward movement
if ( leftY > 0 )
    movement *= forwardSpeed * leftY;
else
    movement *= backwardSpeed * leftY;

// Up/down movement
movement.y += horizontalSpeed * rightY;

// rotation
thisTransform.Rotate( 0, leftX*rotationSpeed * Time.deltaTime, 0, Space.World );

// Assign the movement
movement *= Time.deltaTime;
character.Move( movement );

With all of that in place, driving the butterfly feels reasonably smooth. It really needs animations, but those are several days away.

I should probably attach a movie or something, but at the pace I am going I don’t really want to stop and spend the time on that.

The simple driving controls have taken about 3 hours because I experimented with several different control styles and needed to research various details about Unity input. That is fine, because it is almost exactly the time I estimated.

Day 4 part 2. Simple Race Course

I have worked with several waypoint systems in various games in the past, so I understand how waypoint systems work generally.

In a racing game, the course is defined by many markers. The AI uses the markers to determine where to drive next. Certain UI elements rely on the markers to determine how far along the course each player is and to provide a helper arrow to the player so they don’t get lost. I have read that the full racing games like Mario Kart or Crash Team Racing have several hundred waypoints on their race tracks.

For my simple race course, I need to create a collection of waypoints that describe the course. In this case I will be satisfied with a small circular course with a bit of horizontal change included.

The first step is to define the waypoint script. I already know that each waypoint is a simple transform node. So I created a script class of WayPoint. The data structure naturally lends itself to being a linked list, so I created a public variable called next that points to the next WayPoint. I’ll need to link them up in the Unity editor, but that isn’t too hard.

Since transforms are usually invisible but I want to see my course, I needed to do something when Unity attempted to draw the gismos. (In Unity, gizmo is the term for the UI elements such as boxes and arrows used during development but not necessarily visible during gameplay.) I also want to draw a line between the nodes.

This is completed with a short script function:

void OnDrawGizmos()
{
    Gizmos.color = new Color(0, 1, 0, 0.3f);
    Gizmos.DrawCube(transform.position, new Vector3(5, 5, 5));
    if (Next != null)
    {
        Gizmos.color = Color.green;
        Gizmos.DrawLine(transform.position, Next.transform.position);
    }
}

I then too the time to add several utility functions, a flag to indicate the starting waypoint, created a model of a torus in Blender so I could see them in game, and otherwise tidied things up.

The next step was to create a series of waypoints in the game.

From here, I created an empty game object, added the WayPoint script, added the graphical ring, and turned the whole bundle into a prefab. Then I created an empty game object to use for my course and named it WaypointContainer. Next I dropped a waypoint in the scene near my lake, and moved it into my container. I duplicated each node, moved them around a little bit, and built what felt like a reasonable course.

racecourse

Next came linking each node together. I opened one node, and then dragged the next node into the script’s link. Then I repeated the process for all of the other nodes. They looked beautiful.

Day 4 part 3. Simple AI

To finish up for the day, I needed the computer to be able to drive the course.

I know this script is going to grow significantly over the course of the project because the AI needs to grow to handle many different situations.

One tricky question is if the computer will follow the same rules as the player. When writing an AI you want an AI that is challenging for the player and has the ability to win, but ultimately you want an AI that will lose.

Many people complain that the computer can cheat. What those people don’t realize is that they don’t want a fair game. In a fair game the computer has all the same information and knowledge of the computer, either the AI will fail horribly because it cannot make guesses and assumptions, or it will will easily because it can directly compute the perfect values for a single-shot win. People don’t want a game that is fair, they want a game that is fun.

In most games this means the AI will need to cheat. When a player is skilled and is far ahead of the computer, the AI might need to drive faster than the player’s maximum speed in order to catch up. When the player is unskilled or far ahead of the player, we want the AI’s maximum speed to slow down to allow the player to catch up.

So since I know I cannot use the same control system I developed for the player earlier in the day, I started out with an AI driving controller.

First, it needs a collection of places to go. So I gave it a public GameObject called WaypointContainer. I can assign the level’s actual waypoint container into this variable to swap out whatever map I’m on.

On the Start function I validate the waypoint container. This means first making sure it is set, and then going through all the Transform components inside the container to validate that they are linked to together. It also gives me the chance to find the one (and only one) node marked as the start.

During my FixedUpdate() function I just make one call, a new function called NavigateTowardWaypoint().

It takes a few passes and a little debugging to get all the details correct, but ultimately I end up with this utility function.

 void NavigateTowardsWaypoint()
 {
     if (WaypointContainer == null)
     {
         Debug.LogError("Waypoint collection is null, cannot navigate towards waypoint.");
         return;
     }

     if (mCurrentWaypoint == null)
     {
         Debug.Log("Attempting to navigate towards a waypoint, but the next waypoint is null.");
         return;
     }

     Vector3 relativePosition = mCurrentWaypoint.transform.position - transform.position;
     Vector3 directionToWaypoint = relativePosition.normalized;
     Vector3 newDirection = Vector3.RotateTowards(transform.forward, directionToWaypoint, 1.0f * Time.deltaTime, 0);
     transform.rotation = Quaternion.LookRotation(newDirection);
     Vector3 movement = transform.forward;
     movement.Normalize();

     movement *= ForwardSpeed;
     movement *= Time.deltaTime;
     mCharacter.Move(movement);

     if(relativePosition.magnitude < ProximityToHit)
     {
         mCurrentWaypoint = mCurrentWaypoint.Next;
     }
 }

It has some bugs, potentially the RotateTowards() function will do some bad things, but for this stage in development I’m fine with that. It is important to remember that this is the first playable milestone.

This driving code is going to be rewritten and adjusted many times over the course of the project. Even though right now the two are different, eventually they will be calling into a common collection of functions and behaviors, so I’m more concerned about getting something trivial in place rather than building the final system.

Developing programs has many parallels with constructing a building. There are actually multiple sets of buildings; there is the scaffolding and temporary parts that get created, put up, and torn down throughout the project, and there is the finished product. When you are working on scaffolding that will be removed, for those parts “good enough” is a very low bar. It is very different than working on the load bearing supports of the building which need to be absolutely solid.

The trick is in learning what is temporary and what is permanent. Very often your temporary code becomes permanent, so it is something to watch out for.

Anyway, after about two hours I have assigned the script to two different game objects, and both are happily self-driving around the course. They travel up and down, left and right, and otherwise do all the things they are supposed to do at this point.

My simple AI is complete, and my day ends slightly ahead of schedule.

Leave a Reply

Your email address will not be published. Required fields are marked *

Time limit is exhausted. Please reload CAPTCHA.

This site uses Akismet to reduce spam. Learn how your comment data is processed.