Sunday, September 21, 2014

Out On The Tiles


Once a certain mega-game shipped, I found myself with a little time in which to return to some Unity prototypes. Something about The Benko isometric project kept drawing me back, but I was frustrated by the control scheme. I wanted to control the main character more directly, and I wanted to be able to use a gamepad to do it. However, I also wanted the game to be playable in-browser by anyone with a keyboard and mouse, which meant a back-up set of controls. Surely, I thought, this would be a snap.

Unity did meet me more than halfway here, because rather than having to plow through a bunch of GetKeyDown type functions, I could use Input.GetAxis, which was automatically mapped to WASD for movement, and could quickly be taught to recognize the trusty 360 gamepad. Getting parity in movement was easy. Getting parity in facing was not quite so easy.

The gamepad’s right thumbstick gave me a pair of clean, normalized Vector3s right out of the box, so I was golden there. The interesting part was getting that same result out of a mouse position. I knew that a ScreenToWorld translation would be involved, but there was more to it. I fooled around for many a Designated Leisure Period trying to covert Quaternions to Vector3s using EulerAngles, exploring all the LookAt, LookTowards, and TurnToLookAtThatGuy functions, and generally achieving jack all. I would find solutions that had the right direction but the wrong magnitude, or solutions that worked great until you moved the player away from the origin point, at which point they broke down. Eventually I bumped my head or something and realized that what I needed was a Ray. I guess I had unconsciously filed Rays away in the part of my knowledge that dealt with collisions, and since I wasn't working explicitly on the physics I didn't make the connection that this was an available tool. But yes, when you need any information on where one point lies in relation to another, and how one object might face another, at an arbitrary distance from the origin, your best bet is to break out the Rays:

void HandleLookInput()
{
    //this func is how we set the lookDirection var every tick
 
    //MOUSE VERSION (first so gamepad can override if plugged in)
    mouseTarget = mouseScript.mouseTarget;
    //subtracting player pos from target gives us correct magnitude, which is then normalized to match gamepad
    Ray lookRay = new Ray(transform.position, (mouseTarget-transform.position).normalized);    
    Debug.DrawRay(transform.position, (mouseTarget-transform.position).normalized, Color.magenta);
    //force y to zero to avoid unwanted bullet fall
    lookDirection = new Vector3(lookRay.direction.x, 0.0f, lookRay.direction.z);

    //GAMEPAD VERSION
    if (isGamepad)
    {
        H_LookInput = Input.GetAxis("LookH");
        V_LookInput = Input.GetAxis("LookV");
        if (((H_LookInput >= 1) || (H_LookInput <= -1)
        || 
        (V_LookInput >= 1) || (V_LookInput <= -1))) 
        {
            lookDirection = new Vector3(H_LookInput, 0, V_LookInput);
        }
    }
    else
    {
        //don't change look dir if player released stick
        lookDirection = lastLookDirection;
    }
//Debug.Log("LOOKDIR : " + lookDirection);
}

The “bullet fall” thing is interesting, and leads into the next part: I had a player and a big purple refrigerator (conceptually a bear) and of course my next prototype impulse was to fire some sort of arrow or bullet at the bear. I think the direction this is moving now is into a kind of “isometric twin stick style action panic swarmer”, where the player fends of waves of attackers, but we’ll see where it ends up.

As usual, I wildly underestimated the challenge of deriving a projectile’s trajectory from a Vector3 representing a direction between two objects. Seems like the same thing… but not on a flat plane where the bullet has to come out of the muzzle of a weapon that could be rotated in any arbitrary direction. This was the place for LookRotation, plus the aforementioned Euler Angles.  I won’t pretend to understand any of that linked page, but I was able to find the examples I needed to rough in a working implementation (this lives in the ranged_weapon script attached to the player’s “bow”):

if (canShoot)
{
    //Debug.Log("yes");
    //make bullet
    //Debug.Log("BANG! " + i);
    //Debug.Log("Creating a bullet at " + transform.position);
    Vector3 targetVec = lookscript.lookDirection;
    Quaternion targetRot = Quaternion.LookRotation(targetVec);
    Quaternion flatTargetRot = Quaternion.Euler(new Vector3(90.0f, targetRot.eulerAngles.y, 0.0f));
    bulletClones[i] = Instantiate(bulletTemplate, transform.position, flatTargetRot) as GameObject;
    currentBulletsAlive++;
    //plant a reference to this weapon in the bullet when it is created
    bullet_base bulletscript = bulletClones[i].GetComponent();
    bulletscript.weapon = transform;
    //throw bullet
    bulletPath = lookscript.lookDirection;
    //actually it makes more sense for the bullet to control its own movevemt
    //but will leave this variable in to ref from the bullet script
    //activating cooldown
    canShoot = false;
}

The canShoot bool is to prevent bullet spam, and is made true on GetButtonUp. The bullet itself carries a script which throws it in the right direction (requiring that neat trick just above where we inject a value into a variable owned by something we just created, like inoculating an infant). The “bullet fall” mentioned above happens because for whatever reason the path to the target can sometimes have a y value of -0.1, which will cause it to fall through the ground plane somewhere in its flight time. The hack above led me to really understand the difference between a solution and a hack in a way that I never had before.

 A bug happens when some assumption you have about some data you have is wrong. If you trace the path by which you received the data backwards to where it diverged from your expectations, then re-derive the data from its sources so as to produce the desired result, you have solved the problem. If instead you take the data in your hand and say “well, regardless of what you think you are, you’ll conform to my expectations now so that I can proceed”, you have a applied a hack. It’s obvious why this is dangerous, and it’s equally obvious that there are any number of reasons why you’d do it anyway: maybe the source of the bug is deep within someone else’s code, or the exigencies of time and budget simply don’t allow for a proper solution. Hacking is about picking your battles, knowing when to do surgery and when to just use a bandage and cross your fingers.

Some collider-based debug statements showed the bullet striking the bear, but the game didn't alert the player. I decided to flash the bear red by changing the material color, to my horror this was permanent after I hit the play button to stop the session. Turns out Materials can be instanced too, and a simple co-routinue times the flash effect:

matInstance = Instantiate(baseMat) as Material;
baseColor = baseMat.color;
renderer.material = matInstance; 

[…]

void OnCollisionEnter(Collision collision)
{
    if (collision.collider.tag == "playerBullet")
    {
        Debug.Log ("enemy hit by player bullet");
        StartCoroutine(DamageFlash());
    }
}

IEnumerator DamageFlash()
{
    matInstance.color = Color.red;
    yield return new WaitForSeconds(0.25f);
    matInstance.color = baseColor;
}

So the bear is hit, and everyone knows it. Coming up next:
  • Health, damage, and death
  • GUI enemy health bars
  • A GUI for the player
  • Some proper character models??
  • A proper “world” of tiles? (Maybe procedurally generated???)
The current build of Project Catalan can be played here.