Sunday, October 13, 2013

The Hard Truths of Hardware


Here, on this rocky outcropping, where starving lizard-birds circle above, we shall call an end to the mobile development branch of this project. The controls work well enough for what they are, and you can beat the first level, although the experience leaves a lot to be desired.

Merging the touch controls into the platformer project was educational in a bunch of ways.  First I learned about preprocessor directives, which allowed me to maintain a single version of the project with the same scripts for all platforms. Unity has some cool custom options here, so I was able to create #if MOBILE to enable touch controls for any arbitrary device.

I also found some ways to improve the existing code, for example when calculating movement I was using Input.GetAxis(“Horizontal”) in every expression, so it was getting called half a dozen times every update. I changed it to call that once at the top of the update, store it in a float, and use that float to do calculations for the rest of the tick. I’m willing to bet that on a modern machine this makes exactly zero difference, but it’s the principle of the thing.

I did find that I had made a few fundamentally good decisions. In the character control script I get input in one place, then immediately abstract it into values like float “velocity” and bool “attackInProgress” and use those to actually do stuff. This made adding a new input scheme really easy because I only had to touch one area of the code to get those initial values right.

The real education, though, was getting under the hood of what it really means to have two buttons in combination with a d-pad style arrow option. The first problem I had was that running (hold green button and arrow) wouldn't work, and I eventually saw that since I had configured both buttons as tap inputs, the game didn't know or care if the green button was being held down. The WebPlayer version reads keyboard keys with both GetButton and GetButtonDown, and has different responses to each, so I needed to expand the touch controls example script to handle both taps and holds for each button. I’ll paste the results at the bottom of this entry.  It took a lot of trial and error and still has some big gaps in functionality. This is apparently a harder problem than I originally thought.

The problem is bigger than just input logic. For example, the run/jump: I found in testing that regardless of how well it worked, the actual gesture the run/jump requires (depressing both green and yellow simultaneously) is awkward at best, I found myself bringing my hand around the device to use forefinger and middle finger, which made my other hand harder to use and just felt wrong.

The run/jump came about because, when messing with basic character movement in the WebPlayer version, I unconsciously knew that there’s nothing more natural on a keyboard than augmenting a letter key with a simultaneous shift key. We sometimes do it Multiple Times in a Sentence. Transpose that motion to a touch screen, and it’s suddenly far less natural. I finally understand how the original NES controller, as clunky as it may look now, was an engineering marvel, affording a wonderfully tactile responsiveness between tap and hold combinations on those two nubs of cheap plastic. Multiple multi-million-dollar franchises owe their existence to the joy that developers were able to wring from that dance of tap-hold, discrete-continuous, now this and that, now that and not this, now both at once, many times a second, and that’s just the player’s right thumb! But I digress.

“Transpose” is an apt word, “translate” also has some truth to it. A flat surface is not a controller, and any control scheme change involves reaching across a conceptual gap, trying to say something that’s not there to say, forgetting how many stairs there are and coming down hard on the landing.

The decision of how far to try and take this was made for me, thank goodness, by a hardware limitation. My pokey old smartphone can only handle two simultaneous touch inputs. That means that if you are running, you can’t jump. This breaks the run/jump, but like I said, executing it isn't much fun in the first place. When I hit that wall, I stepped back for a better look at the project. What was I going for here? This was originally and essentially still is a portfolio piece, playable on a web site, and adding an optional downloadable .apk was just an additional bit of frosting. You’d still need a 3rd party .apk installer to use it, and I don't really expect anyone to do so. It was more a proof of concept to say that I could do that work as well, and I think I've delivered that. I have a little more knowledge and a few more ideas for new projects. I’m very interested in the idea of mobile indie games, and now that world doesn't seem quite so foreign or daunting. I’m happy I took the time, and happier still to be shutting that tangent down in order to wrap the main game up once and for all.
using UnityEngine;
using System.Collections;

public class touchControls : MonoBehaviour {
#if MOBILE
 
 //assign 128px textures in inspector
 public Texture leftArrowTex;
 public Texture rightArrowTex;
 public Texture greenButtonTex;
 public Texture yellowButtonTex;
 
 //consumable button state values
 public bool leftFlag
 {
  get{return left;}
 }
 public bool rightFlag
 {
  get{return right;}
 }
 public bool greenTapFlag
 {
  get{return greenTap;}
 }
 public bool greenHoldFlag
 {
  get{return greenHold;}
 }
 public bool yellowTapFlag
 {
  get{return yellowTap;}
 }
 public bool yellowHoldFlag
 {
  get{return yellowHold;}
 }
 private bool left;
 private bool right;
 private bool greenTap;
 private bool greenHold;
 private bool yellowTap;
 private bool yellowHold;
 
 private Rect[] Arrows;
 private Rect[] Buttons;
 private Rect leftArrow;
 private Rect rightArrow;
 private Rect greenButton;
 private Rect yellowButton;

 private float sw; //screen width
 private float sh; //screen height
 private float bu; //boxUnit, default box measurement
 private float au; //arrowUnit, default arrow measurement 
 private Vector3 touchPos; //touch input gives us this
 private Vector2 screenPos; //gui rects need this
 
 private string debugStr;
 private WafhPlayer playscr;
 private int tCount;
 
 void Start () {
  
  sw = Screen.width;
  sh = Screen.height;
  bu = 256;
  au = 128; 

  leftArrow = new Rect(0, sh-au, au, au);
  rightArrow = new Rect(au, sh-au, au, au);
  greenButton = new Rect(sw-(au*2), sh-au, au, au);  
  yellowButton = new Rect(sw-au, sh-au, au, au);
  
  Arrows = new Rect[]{leftArrow, rightArrow};
  Buttons = new Rect[]{greenButton, yellowButton};
    
  playscr = GameObject.FindWithTag("Player").GetComponent();
  
  debugStr = "LEFT =" + leftFlag + "\nRIGHT =" + rightFlag
   + "\nGREENTAP = " + greenTapFlag + "\nGREENHOLD = " + greenHoldFlag 
    + "\nYELLOWTAP =" + yellowTapFlag + "\nYELLOWHOLD = " + yellowHoldFlag
    +"\nhaxis = " + playscr.pHaxis + "\nisRunning = " + playscr.isRunning
    + "\nTCOUNT = " + tCount;

 }

 void Update () {
  tCount = Input.touchCount;
  HandleArrows();
  HandleButtons();
  debugStr =" LEFT =" + leftFlag + "\nRIGHT =" + rightFlag
   + "\nGREENTAP = " + greenTapFlag + "\nGREENHOLD = " + greenHoldFlag 
    + "\nYELLOWTAP =" + yellowTapFlag + "\nYELLOWHOLD = " + yellowHoldFlag
    +"\nhaxis = " + playscr.pHaxis + "\nisRunning = " + playscr.isRunning
    + "\nTCOUNT = " + tCount;
 }
 
 void OnGUI () {
  GUI.Box(new Rect(sw/2-(bu/2), 0, bu, bu), debugStr); 
  GUI.Box (leftArrow, leftArrowTex);
  GUI.Box (rightArrow, rightArrowTex);
  GUI.Box(greenButton, greenButtonTex);
  GUI.Box(yellowButton, yellowButtonTex);  
 }
 
 void HandleArrows()
 {
  if (Input.touchCount > 0)
  {
   foreach (Rect rect in Arrows)
   {
    foreach (Touch touch in Input.touches)
    {
     touchPos = touch.position;
     screenPos = new Vector2(touchPos.x, sh-touchPos.y);
     if(rect.Contains(screenPos))
     {
      if (rect == leftArrow)
      {
       if (touch.phase == TouchPhase.Ended)
       {
        left = false;
       }
       else
       {
        left = true;
       }
      }
      if (rect == rightArrow)
      {
       if (touch.phase == TouchPhase.Ended)
       {
        right = false;
       }
       else
       {
        right = true;
       }       
      }
     }
     else
     {
      if (ButtonlessTouch(screenPos))
      {
       if (rect == leftArrow)
       {
        left = false;
       } 
       if (rect == rightArrow)
       {
        right = false;
       } 
      }
     }
    }
   }
  }
  else //no touches recorded this update
  {
   left = false;
   right = false;
  }
 }
 void HandleButtons()
 {
  if (Input.touchCount > 0)
  {
   foreach (Rect rect in Buttons)
   {
    foreach (Touch touch in Input.touches)
    {
     touchPos = touch.position;
     screenPos = new Vector2(touchPos.x, sh-touchPos.y);
     if(rect.Contains(screenPos))
     {
      if (rect == greenButton)
      {
       if (touch.phase == TouchPhase.Ended)
       {
        greenHold = false;
       }
       else
       {
             
        greenHold = true;
        if (touch.phase != TouchPhase.Began)
        {
         greenTap = false;
        }
        else
        {
         greenTap = true;
        }
       }
       
      }
      if (rect == yellowButton)
      {
       if (touch.phase == TouchPhase.Ended)
       {
        yellowHold = false;
       }
       else
       {
        yellowHold = true;
        if (touch.phase == TouchPhase.Began)
        {
         yellowTap = true;
        }
        else
        {
         yellowTap = false;
        }
       }       
      }      
     }
     else
     {
      if (ArrowlessTouch(screenPos))
      {
       if (rect == greenButton)
       {
        greenTap = false;
        greenHold = false;
       }
      
       if (rect == yellowButton)
       {
        yellowTap = false;
        yellowHold = false;
       }
      }
     }
    }
   }
  }
  else //no touches recorded this update
  {
   greenTap = false;
   greenHold = false;
   yellowTap = false;
   yellowHold = false;
  }
 }
 
 bool ButtonlessTouch(Vector2 screenPos)
 {
  bool buttonless = false;
  if  
    (!greenButton.Contains(screenPos) &&
    !yellowButton.Contains(screenPos))
  {
   buttonless = true;
  }
  return buttonless;   
 }
 
 bool ArrowlessTouch(Vector2 screenPos)
 {
  bool arrowless = false;
  if
   (!leftArrow.Contains(screenPos) &&
    !rightArrow.Contains(screenPos))
  {
   arrowless = true;
  }
  return arrowless;
 }
 
 
 
#endif
}




No comments:

Post a Comment