Wednesday, January 15, 2014

The Terrible Twos


I think level two is looking pretty good, and I'm only about a month and a half behind schedule and have just a few major features to implement, and I have plenty of time and mental energy after my psychically non-demanding job to pour into all my portfolio projects...

Well I do think it looks alright, compared to the opening level. I'm learning. When designing level one, I had just discovered some tutorials on making grids of tiles in Unity, and was enraptured by this idea of a "tile engine" and all the games I was going to make with it, and somehow got it into my head that the various buildings all had to be made of tiles, and somehow all the tiles had to draw from a single 256x256 sheet, so all those brick buildings in the first level are made up of instances of like BRICK_BLDG_LOWER_LEFT_WINDOW tiles and it's horrible. The great joke is that I thought this was somehow saving me memory as I blithely constructed fifty different materials all drawing from the same tile sheet. In the end I don't think it matters much but I made a lot of extra work for myself to produce something that could have looked better.

In a way, though, I did make a bit of a tile engine, because when it was time for level two, I duplicated level one and just took everything out, save for the backgrounds and a single tile for the player to land on when I pressed start. I wanted to sculpt out a shape for the walkable areas of the level, and I started duplicating that tile and moving it around. Soon, I found I wanted images of the mid-ground scenery, I think of these in my mind as "flats", the is from theater, where it means a large frame built from plywood, with canvas stretched over it, on which is painted something like a castle battlement, or a storefront, or a brick wall.

A flat here is just a gameObject with a Mesh Filter and Mesh Renderer components. The Mesh is a Quad, and the renderer's material is made to point at a Texture of X by X (powers of two, please) and you have onscreen whatever it is you drew in your favorite "draw stuff" program. Just place these further away on the z-axis than your player and the ground he or she must collide with, and you have 2d scenery.

Glossing quickly over the quality of the art itself (yyyyeah), we come to the matter of the ghost, who presented a particular problem. Enemies in WAFHGame execute a function called "Patrol", and enemies are a sub-species of character, that is to say the script WafhEnenmy.cs is a class which inherits from WafhChar.cs. The problem is that WafhChar only executes the Patrol function during its update if the Char in question is Grounded. 

Being Grounded in Unity is a particular state, like being flat-footed in D&D. (if you get that reference, I'm sorry). The Character Controller is Unity's answer to one of the common beginner game dev problems: "I need some object that represents the player than can listen for all sorts of input, and respond naturally to all sorts of things, and inteact with both the environment and anyone else around, in all possible types of games." A lot of people seem wary of the Character Controller, but for a lot of us it's a godsend and we wouldn't be here without it, honest to Bob. Code low-level game controller input systems? Are you out of your mind?

So here we had a ghost who would not Patrol, because since I set his character's gravity to 0, to keep him from falling, he never acquired the property of being Grounded, so he would never Patrol. First thought was to allow Patrol to fire when not Grounded. Now, as I'm writing this, I wonder "why didn't I just remove the Patrol check from the conditional of being Grounded? Let falling skeletons Patrol in the air for a few fractions of a second when they fall off a platform, they're not hurting anyone." A noble thought, and yet my baser natures won out. My memory grows dim. I can say for sure I ended up extending WAFHGhost from WAFHChar: 


public class wafhGhost : WafhEnemy {
 //ghostly vars:
 float waverYVariance = 16.0f; //how spooky do we get
 float maxY;      //calcd from current position
 float minY;      //calcd from current position
 Vector3 currentPos;
 float InitialY;     //source for calcs
 float currentY;     //source for calcs
 float targetY;     //where we are going
 float targetDistanceDelta = 1.0f; //dunno
 CapsuleCollider capsule;
 Vector3 vertTarget;
 Vector3 myCenter;
 Vector3 ghostVelocity;
 float ghostYSpeed;
 
 //public GameObject debugCubePrefab;
 //GameObject debugCubeClone;
 
 public override void Start () {
  
  base.Start();
  gravity = 0.0f;
  InitialY = transform.position.y;
  maxY = InitialY + waverYVariance;
  minY = InitialY - waverYVariance;
  capsule = transform.GetComponent();
  //debugCubeClone = Instantiate (debugCubePrefab, transform.position, transform.rotation) as GameObject;
  ghostYSpeed = 50.0f;
  deathOffset = new Vector2(0, 0);
 }
 

 public override void Update () {
  base.Update();
  myCenter = new Vector3(transform.position.x + (controller.center.x), transform.position.y + (controller.center.y), transform.position.z + (controller.center.z));
  GhostlyShenanigans();
  ghostPatrol(patrolLimit);
  //Debug.DrawRay(myCenter, debugCubeClone.transform.position, Color.green, 0.1f);
  
 }
 
 void GhostlyShenanigans()
 {
  currentY = transform.position.y;
  AdjustGhostVelocity();
  GhostFloat(transform.position.y);
 }
 
 public override void Patrol(float patrolLimit)
 {
  base.Patrol(patrolLimit);
 }
 void AdjustGhostVelocity()
 {
  if (transform.position.y > maxY)
  {
   //Debug.Log("broke max, moving down");
   ghostYSpeed = -ghostYSpeed;
   
  }
  if (transform.position.y < minY)
  {
   //Debug.Log("broke min, moving up");
   ghostYSpeed = -ghostYSpeed;
  }
  ghostVelocity = new Vector3(0.0f, ghostYSpeed, 0.0f);
 }
  
 void GhostFloat(float currentY)
 {
  //transform.position = Vector3.MoveTowards(transform.position, vertTarget, targetDistanceDelta);
  controller.Move( ghostVelocity * Time.deltaTime );
 }
 


 void ghostPatrol(float patrolLimit)
 {
  state = CharacterState.Walking;
  patrolTime += Time.deltaTime;
  //print ("patrol time " + patrolTime);
  if (patrolTime < patrolLimit)
  {
   //the ifs in here are to handle enemies with flipped textures cause I suck at art asset generation
   if (facing == 1)
   {
    velocity.x = walkSpeed; 
    if (gameObject.tag == "enemy")
    {
     ragePixel.SetHorizontalFlip(true);       //texture flip
    }
    else
    {
     ragePixel.SetHorizontalFlip(false);
    }
    ragePixel.PlayNamedAnimation("WALK", false);    //texture animation
   }
   if (facing == -1)
    {
     velocity.x = -walkSpeed;
     if (gameObject.tag == "enemy")
    {
     ragePixel.SetHorizontalFlip(false);       //texture flip
    }
    else
    {
     ragePixel.SetHorizontalFlip(true);
    }
     ragePixel.PlayNamedAnimation("WALK", false);    //texture animation
   }
  }
  else
  {
   //print ("FLIP FUCKER");
   patrolTime = 0.0f;
   facing = -facing;
  }
 }
}



We are Patrolling every Update, but this is really the same RagePixel code we use in the main Patrol function, it's a copy/paste. This, as far as I have gathered, is one of the sadder and more shameful sins you can commit writing code: having to write something out in more than one place. It is an invariable sign that you have failed to optimize, misaligned your architecture, fucked up in general.

None of this is what I was meaning to write a post about. I meant to write about a particular revelation during the last phase, the placing of the bus transfers, the deciding which branches should have enemies walking (or floating) back and forth on them: I sat down to the business of level design and realized I had already made most of the important decisions already.

"Level Designer" is the cap I like to picture myself wearing, and it's a role I've made a decent living holding before, and I've often thought of the level design "parts" of making WAFHGame as satanically delicious treats to gorge on when I had finished the Volga Boatman gruntwork of asset preparation. Well, as it turns out, when I decided the size of the house and store flats, that was level design, because that, combined with the speed of the player (also level design) determines how fast the scenery moves by, which affects how your eye registers it and both how much attention you pay to it and at least part of your emotional response. Drawing the trees, and deciding how far apart branches should be, that was level design, so is the scroll speed on the parallax skyscrapers. Anything that happens to you in the level is by definition a result of level design, at some level.

Of course for tiny projects, level and gameplay design are inseparable, but I think it's an easy thing to lose sight of as projects get bigger. The size of the tile and the length of the jump are bigger level design factors than anything else in WAFHGane, and it's impossibl to make any level design decisions that don't depend omn those. I keep trying to adjust  the jump as I go, and it often feels unsalvageable and floaty and tedious, and I want to start over, but of course that's insanity. It just has to get done, but every decision, even the most frivolous-seeming art decision (sorry artists) has direct effects on gameplay and are a core component of level design. As someone like Timothy Leary might have said: "The more I get into it, the more it's all one thing".

Three levels, with different art. Each level has a pair of enemy types. Some enemy types repeat, but pairs don't. There are a few unlocks you can get by beating levels in particular ways. The experience flows smoothly, from menu to gameplay, with appropriate UI popups, without crashing.

That's it, not even a leaderboard, no points even! How hard could it be. And yet here I sit, blogging. Not working! Not drawing skeletons! Shame on me for reals. I'm 2/3 done.