Thursday, December 13, 2012

To Lose One's Self Technical Postmortem

Hello again,

This past semester I took TC 445 at Michigan State University. I'm proud to present my final from this class, To Lose One's Self, which can be played here:

http://class.cas.msu.edu/tc445/level4/group3/

I wanted to talk about some of the coding design and implementation decisions I made in this project. The premise of the main mechanic is pretty simple. You are in an unbeatable maze. You must find your way to the exit. You have the ability to make the walls of the maze disappear for a time, however there is an enemy hunting you. When you walls are down, you can go right to the exit, but he can go right to you. You must balance dropping the walls in such a way as to avoid the enemy, but still reach the exit before he gets you. Along the way, there are objects you can collect that will make him slower. If you collect all 6 and beat it, you get the happy ending. If you beat it without getting all 6, you get the not so happy ending. The game was made in Unity3D version 3.5 in a time period of roughly 5 weeks. That's all I'll say about the game itself, check it out!!

I was the only programmer on this project, in fact I was the only one who had much coding knowledge at all. I was very fortunate to work with some really great, smart, passionate artists and designers on this project as well, which worked out great. As the only programmer, I pretty much had alot power to do things how I wanted. 

The game pretty much has two main states, walls and no walls. When the walls are up, the enemy should be navigating the maze to try to find you, and the walls of the maze should all be there with colliders turned on and we have a certain skybox. When the walls are down, the walls should animate them into the ground, the enemy should run straight towards you, and the skybox should change as well as some visual FX should be applied. I made a GameManager class to keep track of this state. When the "F" key is pressed, the transition occurs and the walls go down. Then after a random amount of time (from about 3 to 5 seconds) the walls go back up and the state is changed. This is all handled by the GameManager. It seemed really, really impractical to me to have the GameManager actually set all the walls and the enemy. Thats alot of work to do in one frame and could easily cause some lag do to looping through all the walls and other object and turning them off or changing their settings. Instead, I made GameManager a singleton that each object affected by the game state constantly asks the manager what they should be doing. Here's some pseudo-code to demonstrate what I mean.

public class Wall
{
   void Update()
   {
         if(GameManager.Instance().AreWallsDown())
         {
               //Do set walls down
         }
         else
         {
               //Do set walls up
         }
   }
   //Rest of implementation
}

By having this concept, everything in the game was consistent, there were no spikes in lag (at least not from code..) when the walls went from up to down visa versa. Everything ran pretty smoothly.

The enemy was a little different, however. He's probably the most interesting piece in this post, so I'll spend some time on him now.

When the walls are down, he should sprint right at you. That's pretty easy to accomplish with a simple seek behavior. But when the walls are up, he should wander around and try to find you. Not so simple now. Any AI programmer's first inclination in this case would be to use an A* algorithm and indeed, it was mine as well. However after thinking about it, the maze is unbeatable. True I can give him a point and make him find his way there as fast as possible, but there are two big issues with this.

1. Where do we tell him to go? Do we just give him random points from our node tree and have him head there? Do we give him the player's position? What if he can't get there (remember the maze is unbeatable..everything is sectioned with no way to get from one to another without the player dropping the walls)

2. Even if we knew what to tell our AI and how to get to the player, we don't want to. An AI that apparently knows the maze and can always find its way to you while you're still trying to figure out which way to go? Not very fair to the player...the AI should technically be as "lost" as the player is. It should maybe know where the player kinda is, and work its way towards him as best as it can, but it should never be able to just magically "go" to the player, even if it can. 

So now, what are we left with? The answer is really quite simple, we chunk A* all together and go with a more Unity styled solution, RayCasts. A raycast is essentially shooting a ray out of a point in a direction and seeing if you hit anything. A full reference can be found here

Using this, we can pretty much do everything we want to. And if we use layering in a smart manner, we can use it pretty efficiently with not a ton of overhead. With this in mind, we can do some simple obstacle avoidance

void AvoidObstacles(Vector3 dir /*the direction I want to go*/)
{
   RaycastHit hit;
   if(Physics.Raycast(transform.position, transform.forward, out hit, ObjectDetectionRange))
   {
       if(hit.transform != transform)
       {
           dir += hit.normal;
       }
   }

   var newRot = Quaternion.LookRotation(dir);
   transform.rotation = Quaternion.Slerp(transform.rotation, newRot, Time.deltaTime * Speed);
   transform.position += transform.forward * Speed * Time.deltaTime;
}

By simply adding the normal of whatever we hit, our enemy will essentially avoid walls. Its not quite this simple, you'll need to shoot more rays to ensure his sides don't clip the walls, but you get the basic idea. Now we need to decide what direction we want to go. When the walls are down, this direction is the direction towards the player. But what about when the walls are up? Well...its just his current forward. He just wants to go forward. He doesn't really know much about whats going on, so if he doesn't know where the player is and he can't see him, he'll just kinda wander forward an avoid the walls of the maze. 

void Update()
{
    Vector3 dir;
    if(GameManager.Instance().AreWallsDown()) dir = Seek(Player.transform.position);
    else dir = Wander(); //returns transform.forward
    AvoidObstacles(dir);
}

That's really about it. Now, there are two special cases to this to make him smarter. 

1. The walls are up and I can see the player. I shouldn't just wander around, I should kill him! (Or get him or...whatever it is I do).

2. I can't see him right now, but I did see him recently. Rather than wander forward aimlessly, I should try to (without an A*) get to where I  last saw him. 

The second one is pretty easy to make look natural. Everytime the enemy sees the player, whether the walls are up or down, he stores the point where he saw him/her. Then next time he can't see the player, rather than just go forward, he does a Seek(LastKnowPosisition). He does till he either times out (he got stuck or he can't get there as the maze won't allow it), or he reaches the point where he last the saw the player. Lets he times out, its possible he took a stupid route and didn't end up where he wanted to go. Or he's not in the same section as his target and he's not gonna be able to get there. Or he was still on his way there. In any case, when he comes to a fork, he ALWAYS take the way towards where he thinks the player is (very natural seeming). And if he didn't get all the way there, then now, as he goes back to wandering, he's alot closer to the player than he would've been otherwise, keeping some tension on the player to not drop those walls so fast again.

The first one is a bit more tricky, but not completely terrible. Basically we need to know two things to know if we can see the player, am I facing the player, and is there anything between us that would obstruct my vision (such as, oh, I don't know, millions of maze walls). If both these conditions are met, I can see the player and should seek him rather than wander forward aimlessly. But how do we know these two things are true? If there's anything obstructing our vision, a raycast will pick that up pretty easily so that's not so bad. As for if I'm facing the player, we will turn to some Vector Calculus. 

bool DoISeeThePlayer()
{
   Vector3 directionTowardsPlayer = (Player.transform.position - transform.position).normalized;
   //If the dot product between my forward and the direction from me that the player lies is greater than
  // zero, I can see him. Instead of zero, we'll use some vision range variable that the designer can set
  //A typical human vision range would be from about 15-30
   if (Vector3.Dot (transform.forward, directionTowardsPlayer) > VisionRange) 
   {
        //I can see the player, now do a raycast to make sure there's no objects between me and him/her
        if (Physics.Raycast (transform.position, directionTowardsPlayer, out hit)) 
        {
              if (hit.transform == Player.transform) 
              {
                   UpdateLastKnowPositionOfPlayer();
                   return true;
              }
         }
   }
   return false;
}

With this logic, I can detect in-maze whether or not I should go after the player.

Well that about wraps this one up, if you have any questions about anything else in the game, feel free to comment or shoot me an email at warddav16@gmail.com. Till next time.

--daviD Ward