Unity Penelope Tutorial update for Unity 3 and C# – Part 2, Pages 37-71

Oct
31

Need Part 1?

Wow, I didn’t realize that part 2 would involve writing so much code. The tap control piece was much longer than I had expected, and even now the zoom in feels much faster than the zoom out so I might have a small error some where.

I also had to rewrite the ZoomCamera JS to C# because the TapControl had a code reference to the class and I didn’t realize that you couldn’t have mismatching code classes reference each other, not that I am that surprised that it was a problem when it arose either mind you… maybe I imagined unity was so magical that it wouldn’t be an issue. ^_~

Part 2 converts all three control types into c# along with 2 other classes:
FollowTransform
CameraRelativeControl
PlayerRelativeControl
TapControl
ZoomCamera

I did run into a few small errors in the tutorial, and I made notes by them above their respected snippet or as a comment in the code. Enjoy.

Part 2 Source Files

Page 37

public Transform targetTransform;
public bool faceForward = false;

private Transform thisTransform;

void Start ()
{
    thisTransform = transform;
}

void Update ()
{
    thisTransform.position = targetTransform.position;
   
    if( faceForward )
    {
        thisTransform.forward = targetTransform.forward;   
    }
}

.
.
Page 39

// this is out side of the class declaration
[RequireComponent( typeof( CharacterController ) )]
/////

public Joystick moveJoystick;
public Transform cameraTransform;
public float speed = 6.0f;

private Transform thisTransform;
private CharacterController character;

void Start ()
{
    thisTransform = (Transform)GetComponent( typeof( Transform ) );
    character = (CharacterController)GetComponent( typeof( CharacterController ) );
}

.
.
Page 40 (top)

void Update ()
{
    Vector3 movement = cameraTransform.TransformDirection( new Vector3( moveJoystick.position.x,
                                                                    0.0f,
                                                                    moveJoystick.position.y ) );
    movement.y = 0.0f;
    movement.Normalize();
}

.
.
Page 40 (bottom)

Vector2 absJoyPos = new Vector2( Mathf.Abs( moveJoystick.position.x ),
                                    Mathf.Abs( moveJoystick.position.y ) );
movement *= speed * ( ( absJoyPos.x > absJoyPos.y ) ? absJoyPos.x : absJoyPos.y );
// below is come code explained not snipped right below the above snippet
movement *= Time.deltaTime;
character.Move(movement);

.
.
page 41

void FaceMovementDirection()
{

    Vector3 horizontalVelocity = character.velocity;
    horizontalVelocity.y = 0;
    if( horizontalVelocity.magnitude > 0.1f )
    {
        thisTransform.forward = horizontalVelocity.normalized; 
    }
}

///// the little bits of code after the above example in book

public Joystick rotateJoystick;
public Transform cameraTransform;
public Transform cameraPivot;
public Vector2 rotationSpeed = new Vector2( 50, 25 );

.
.
Page 42

Vector2 camRotation = rotateJoystick.position;

camRotation.x *= rotationSpeed.x;
camRotation.y *= rotationSpeed.y;
camRotation *= Time.deltaTime;

cameraPivot.Rotate( 0, camRotation.x, 0, Space.World );
cameraPivot.Rotate( camRotation.y, 0, 0 );

.
.
Page 42

public float jumpSpeed = 16.0f;
public float inAirMultiplier = 0.25f;

private Vector3 velocity;

// add below in the update function

if( character.isGrounded )
{
    if( rotateJoystick.tapCount == 2 )
    {
        velocity = character.velocity;
        velocity.y = jumpSpeed;
    }
}
else
{
    velocity.y += Physics.gravity.y * Time.deltaTime;
    movement.x *= inAirMultiplier;
    movement.z *= inAirMultiplier;
}

movement += velocity;
movement += Physics.gravity;


// after faceMovementDirection()
if( character.isGrounded )
{
    velocity = Vector3.zero;
}

.
.
Page 44

void OnEndGame()
{
    moveJoystick.Disable();
    rotateJoystick.Disable();
    this.enabled = false;
}

.
.
Page 48

public Joystick moveJoystick;
public Joystick rotateJoystick;
public Transform cameraPivot;
public float forwardSpeed = 6;
public float backwardSpeed = 3;
public float sidestepSpeed = 4;
public float jumpSpeed = 4;
public float inAirMultiplier = 0.25f;
public Vector2 rotationSpeed = new Vector2( 50, 25 );

private Transform thisTransform;
private CharacterController character;
private Vector3 cameraVelocity;
private Vector3 velocity;

.
.
Page 49

void Start ()
{
    thisTransform = (Transform)GetComponent( typeof(Transform) );
    character = (CharacterController)GetComponent( typeof(CharacterController) );
}

void OnEndGame()
{
    moveJoystick.Disable();
    rotateJoystick.Disable();
    this.enabled = false;
}

.
.
Page 49-50

Vector3 movement = thisTransform.TransformDirection( new Vector3( moveJoystick.position.x,
                                                                    0,
                                                                    moveJoystick.position.y ) );
movement.y = 0;
movement.Normalize();

//
// Tutorial cuts in here: Next, we apply movement...
//

Vector3 cameraTarget = Vector3.zero;
Vector2 absJoyPos = new Vector2( Mathf.Abs( moveJoystick.position.x ),
                                    Mathf.Abs( moveJoystick.position.y ) );
if( absJoyPos.y > absJoyPos.x )
{
    if( moveJoystick.position.y > 0 )
    {
        movement *= forwardSpeed * absJoyPos.y;
    }
    else
    {
        movement *= backwardSpeed * absJoyPos.y;
        cameraTarget.z = moveJoystick.position.y * 0.75f;
    }
}
else
{
    movement *= sidestepSpeed * absJoyPos.x;
    cameraTarget.x = -moveJoystick.position.x * 0.5f;
}

.
.
Page 51

if( character.isGrounded )
{
    if( rotateJoystick.tapCount == 2 )
    {
        velocity = character.velocity;
        velocity.y = jumpSpeed;
    }
}
else
{
    velocity.y += Physics.gravity.y * Time.deltaTime;
    cameraTarget.z = -jumpSpeed * 0.25f;
    movement.x *= inAirMultiplier;
    movement.z *= inAirMultiplier;
}

movement += velocity;
movement += Physics.gravity;
movement *= Time.deltaTime;
character.Move( movement );

if( character.isGrounded )
{
    velocity = Vector3.zero;
}

.
.
Page 52 (top)
This is a touch different, the third parameter needs a ref keyword.

Vector3 pos = cameraPivot.localPosition;
pos.x = Mathf.SmoothDamp( pos.x, cameraTarget.x, ref cameraVelocity.x, 0.3f );
pos.z = Mathf.SmoothDamp( pos.z, cameraTarget.z, ref cameraVelocity.z, 0.5f );
cameraPivot.localPosition = pos;

.
.
Page 52 (middle)

if( character.isGrounded )
{
    var camRotation = rotateJoystick.position;
    camRotation.x *= rotationSpeed.x;
    camRotation.y *= rotationSpeed.y;
    camRotation *= Time.deltaTime;
    thisTransform.Rotate( 0, camRotation.x, 0, Space.World );
    cameraPivot.Rotate( camRotation.y, 0, 0 );
}

.
.
Page 56,57,58

public enum ControlState
{
    WaitingForFirstTouch,
    WaitingForSecondTouch,
    MovingCharacter,
    WaitingForMovement,
    ZoomingCamera,
    RotatingCamera,
    WaitingForNoFingers
}

public float minimumTimeUntilMove = 0.25f;
public bool zoomEnabled = true;
public float zoomEpsilon = 25.0f;
public bool rotateEnabled = true;
public float rotateEpsilon = 10.0f;

private int state = (int)ControlState.WaitingForFirstTouch;
private int[] fingerDown = new int[ 2 ];
private Vector2[] fingerDownPosition = new Vector2[ 2 ];
private int[] fingerDownFrame = new int [ 2 ];
private float firstTouchTime;

void ResetControlState()
{
    state = (int)ControlState.WaitingForFirstTouch;
    fingerDown[ 0 ] = -1;
    fingerDown[ 1 ] = -1;
}

void Start ()
{
    ResetControlState();
}

void OnEndGame()
{
    this.enabled = false;
}

void Update ()
{
    int touchCount = Input.touchCount;
   
    if( touchCount == 0 )
    {
        ResetControlState();
    }
    else
    {
        // everything else is going to go here
    }
}

.
.
Page 59

int i;
Touch touch;
Touch[] touches = Input.touches;
Touch touch0 = new Touch(); // have to assign a default value to avoid compile time error;
Touch touch1 = new Touch();
bool gotTouch0 = false;
bool gotTouch1 = false;

if( state == (int)ControlState.WaitingForFirstTouch )
{
    for( i = 0; i < touchCount; i++ )
    {
        touch = touches[ i ];
       
        if( touch.phase != TouchPhase.Ended &&
            touch.phase != TouchPhase.Canceled )
        {
            state = (int)ControlState.WaitingForSecondTouch;
            firstTouchTime = Time.time;
            fingerDown[ 0 ] = touch.fingerId;
            fingerDownPosition[ 0 ] = touch.position;
            fingerDownFrame[ 0 ] = Time.frameCount;
            break;
        }
    }
}

.
.
Page 60

if( state == (int)ControlState.WaitingForSecondTouch )
{
    for( i = 0; i < touchCount; i++ )
    {
        touch = touches[ i ];
        if( touch.phase != TouchPhase.Canceled )
        {
            if( touchCount >= 2 &&
                touch.fingerId != fingerDown [ 0 ] )
            {
                state = (int)ControlState.WaitingForMovement;
                fingerDown[ 1 ] = touch.fingerId;
                fingerDownPosition[ 1 ] = touch.position;
                fingerDownFrame[ 1 ] = Time.frameCount;
                break;
            }
            else if ( touchCount == 1 )
            {
                Vector2 deltaSinceDown = touch.position - fingerDownPosition[ 0 ];
                if( touch.fingerId == fingerDown[ 0 ] &&
                    ( Time.time > firstTouchTime + minimumTimeUntilMove ||
                        touch.phase == TouchPhase.Ended ) )
                {
                    state = (int)ControlState.MovingCharacter;
                    break;
                }
            }  
        }
    }
}

.
.
Page 61, 62, 63
I got a compile time error because of touch0 and touch1 being unassigned… but they get assigned in the Boolean logic above it, I had to assign them to default object to continue… but I don’t feel that is the correct way to progress.

if( state == (int)ControlState.WaitingForMovement )
{
    for( i = 0; i < touchCount; i++ )
    {
        touch = touches[ i ];
        if( touch.phase == TouchPhase.Began )
        {
            if( touch.fingerId == fingerDown[ 0 ] &&
                fingerDownFrame[ 0 ] == Time.frameCount )
            {
                touch0 = touch;
                gotTouch0 = true;
            }
            else if( touch.fingerId != fingerDown[ 0 ] &&
                    touch.fingerId != fingerDown[ 1 ] )
            {
                fingerDown[ 1 ] = touch.fingerId;
                touch1 = touch;
                gotTouch1 = true;
            }
        }
       
        if( touch.phase == TouchPhase.Moved ||
        touch.phase == TouchPhase.Stationary ||
        touch.phase == TouchPhase.Ended )
        {
            if( touch.fingerId == fingerDown[ 0 ] )
            {
                touch0 = touch;
                gotTouch0 = true;
            }
            else if( touch.fingerId == fingerDown[ 1 ] )
            {
                touch1 = touch;
                gotTouch1 = true;
            }
        }
    }
   
    if( gotTouch0 )
    {
        if( gotTouch1 )
        {
            Vector2 originalVector = fingerDownPosition[ 1 ] - fingerDownPosition[ 0 ];
            Vector2 currentVector = touch1.position - touch0.position;
            Vector2 originalDir = originalVector / originalVector.magnitude;
            Vector2 currentDir = currentVector / currentVector.magnitude;
            float rotationCos = Vector2.Dot( originalDir, currentDir );
           
            if( rotationCos < 1 )
            {
                float rotationRad = Mathf.Acos( rotationCos );
               
                if( rotationRad > rotateEpsilon * Mathf.Deg2Rad )
                {
                    state = (int)ControlState.RotatingCamera;
                }
            }
           
            if( state == (int)ControlState.WaitingForMovement )
            {
                float deltaDistance = originalVector.magnitude - currentVector.magnitude;
               
                if( Mathf.Abs( deltaDistance ) > zoomEpsilon )
                {
                    state = (int)ControlState.ZoomingCamera;
                }
            }
        }
    }
    else
    {
        state = (int)ControlState.WaitingForNoFingers;
    }
}

.
.
Page 64

if( state == (int)ControlState.RotatingCamera ||
state == (int)ControlState.ZoomingCamera )
{
    for( i = 0; i < touchCount; i++ )
    {
        touch = touches[ i ];
       
        if( touch.phase == TouchPhase.Moved ||
            touch.phase == TouchPhase.Stationary ||
            touch.phase == TouchPhase.Ended )
        {
            if( touch.fingerId == fingerDown[ 0 ] )
            {
                touch0 = touch;
                gotTouch0 = true;
            }
            else if( touch.fingerId == fingerDown[ 1 ] )
            {
                touch1 = touch;
                gotTouch1 = true;
            }
        }
    }
   
    if( gotTouch0 )
    {
        if( gotTouch1 )
        {
            // Call our Camera Control Function
        }
    }
    else
    {
        state = (int)ControlState.WaitingForNoFingers;
    }
}

// Character control happens at the end of the update function

.
.
Page 65 – 66
the AnimaitonControl class is defined here, but it hasn’t been created yet.
Also this is the function where I realized I had to rewrite the ZoomCamera class.

public GameObject cameraObject;
public Transform cameraPivot;
public GUITexture jumpButton;
public float speed;
public float jumpSpeed;
public float inAirMultiplier = 0.25f;
public float minimumDistanceToMove = 1.0f;
public float zoomRate;

private ZoomCamera zoomCamera;
private Camera cam;
private Transform thisTransform;
private CharacterController character;
//private AnimationController animationController;
private Vector3 targetLocation;
private bool moving = false;
private float rotationTarget;
private float rotationVelocity;
private Vector3 velocity;

// put the following in the start function before ResetControlState call.
thisTransform = transform;
zoomCamera = (ZoomCamera)cameraObject.GetComponent( typeof(ZoomCamera) );
cam = cameraObject.camera;
character = (CharacterController)GetComponent( typeof(CharacterController) );

void FaceMovementDirection()
{
    Vector3 horizontalVelocity = character.velocity;
    horizontalVelocity.y = 0;
    if( horizontalVelocity.magnitude > 0.1 )
    {
        thisTransform.forward = horizontalVelocity.normalized;
    }
}

.
.
Page 67
the tutorial was missing the definition for the var ray, looked at the final JS code and added it in.

void CharacterControl()
{
    int count = Input.touchCount;
   
    if( count == 1 && state == (int)ControlState.MovingCharacter )
    {
        Touch touch = Input.GetTouch(0);
       
        if( character.isGrounded && jumpButton.HitTest( touch.position ) )
        {
            velocity = character.velocity;
            velocity.y = jumpSpeed;
        }
        else if( !jumpButton.HitTest( touch.position ) && touch.phase != TouchPhase.Began )
        {
            RaycastHit hit;
            Ray ray = cam.ScreenPointToRay( new Vector3( touch.position.x, touch.position.y ) );
            if( Physics.Raycast( ray, out hit ) )
            {
                float touchDist = ( transform.position - hit.point ).magnitude;
                if( touchDist > minimumDistanceToMove )
                {
                    targetLocation = hit.point;
                }
                moving = true;
            }
        }
    }
}

.
.
Page 68, 69
Now after this part in the tutorial they ask you to try it. Make sure you Player object has some values set for speed and jumpSpeed other than 0 or it may seem like nothing is working.

        Vector3 movement = Vector3.zero;
       
        if( moving )
        {
            movement = targetLocation - thisTransform.position;
            movement.y = 0;
            float dist = movement.magnitude;
            if( dist < 1 )
            {
                moving = false;
            }
            else
            {
                movement = movement.normalized * speed;
            }
        }
       
        if( !character.isGrounded )
        {
            velocity.y += Physics.gravity.y * Time.deltaTime;
            movement.x *= inAirMultiplier;
            movement.z *= inAirMultiplier;
        }
       
        movement += velocity;
        movement += Physics.gravity;
        movement *= Time.deltaTime;
        character.Move( movement );
       
        if( character.isGrounded )
        {
            velocity = Vector3.zero;
        }
       
        FaceMovementDirection();

.
.
Page 69 (bottom) 70 (top)
don’t for get to add CameraControl function call where the comment you made previously told you to put it ^_^ Make sure values for TapControl have some default values, thought stuff wasn’t working until I noticed things with values of 0.

void CameraControl( Touch touch0, Touch touch1 )
{
    if( rotateEnabled && state == (int)ControlState.RotatingCamera )
    {
        // rotate stuff
        Vector2 currentVector = touch1.position - touch0.position;
        Vector2 currentDir = currentVector / currentVector.magnitude;
        Vector2 lastVector = ( touch1.position - touch1.deltaPosition) -
                                ( touch0.position - touch0.deltaPosition );
        Vector2 lastDir = lastVector / lastVector.magnitude;
        float rotationCos = Vector2.Dot( currentDir, lastDir );
       
        if( rotationCos < 1 )
        {
            Vector3 currenctVector3 = new Vector3( currentVector.x, currentVector.y );
            Vector3 lastVector3 = new Vector3( lastVector.x, lastVector.y );
            float rotationDirection = Vector3.Cross( currenctVector3, lastVector3 ).normalized.z;
            float rotationRad = Mathf.Acos( rotationCos );
            rotationTarget += rotationRad * Mathf.Rad2Deg * rotationDirection;
           
            if( rotationTarget < 0 )
            {
                rotationTarget += 360;
            }
            else if( rotationTarget >= 360 )
            {
                rotationTarget -= 360;
            }
        }
    }
    else if( zoomEnabled && state == (int)ControlState.ZoomingCamera )
    {
        // zooming stuff
        float touchDistance = ( touch1.position - touch0.position ).magnitude;
        float lastTouchDistance = ( ( touch1.position - touch1.deltaPosition ) -
                                        ( touch0.deltaPosition - touch0.deltaPosition ) ).magnitude;
        float deltaPinch = touchDistance - lastTouchDistance;
       
        zoomCamera.zoom += deltaPinch * zoomRate * Time.deltaTime;
    }
}

.
.
Page 71 (bottom)
Here is another temp value since the JS version is modifying some protected values.

void LateUpdate()
{
    Vector3 tempVector3 = cameraPivot.eulerAngles;
   
    tempVector3.y = Mathf.SmoothDampAngle( cameraPivot.eulerAngles.y,
                                                        rotationTarget,
                                                        ref rotationVelocity,
                                                        0.3f );
    cameraPivot.eulerAngles = tempVector3;
}

Part 2 Source files