../_images/Cover2.jpg

Player Control

Jaw Rotation

For the convenience of development, the idle of the whale has been divided into two parts:

  • the body part (static animation which will not be interacting with any user input)
  • the jaw part (which will rotate according to rheostat value)
whale_body jaw

Then, open the jaw to maximum of 60 degrees and map this to the maximum angle range the patient could open his leg (if the user chose Right leg mode of both leg mode, the system map the jaw open angle onto angle_r from Arduino Serial reading and vice versa):

// Jaw.cs (... represents other code blocks irrelevant to the current session)

...

[SerializeField] private bool isRightLeg;

private float angleJaw; // whale jaw open angle controlled by leg open angle

...

void Update () {
    ...

    if (isRightLeg) {
        angleJaw = arduinoHelper.angle_r / (calibrationMenu.angleRightConstraint / 60f);
    }
    else {
        angleJaw = arduinoHelper.angle_l / (calibrationMenu.angleRightConstraint / 60f);
    }

    PotentiometerControl(angleJaw);

    ...
}

...

// ----- Arduino Potentiometer Control -----

void PotentiometerControl (float angle) {
    transform.localRotation = Quaternion.Euler(0, 0, -angle);
}

Whale Body Movement

In order to make the whale idle including whate body without jaw and the jaw moving at the same time, the two Prefabs have been wrapped in a parent GameObject of Whale and the script has been executed onto the parent object as well:

../_images/Whale.jpg

The implementation of the whale body movement starts with defining the 3 plausible states a current whale could have:

// Whale.js

enum State {
    movingDown,
    movingUp,
    stop
}

Due that the up and down movements are continous rather then instantly, the parallel running of up-down movements and other implementations have been processed simultaneously using Asynchronous Programming:

// Whale.js (... represents other code blocks irrelevant to the current session)

private void MovementHandler() {
    switch (state) {
        case State.movingDown:
            transform.Translate(
                -Vector3.up * speed * Time.deltaTime,
                Space.World);
            break;
        case State.movingUp:
            transform.Translate(
                Vector3.up * speed * Time.deltaTime,
                Space.World);
            break;
        case State.stop:
            // stop the whale by assign the current position to its position
            transform.position = gameObject.transform.position;
            break;
        default:
            transform.position = gameObject.transform.position;
            break;
    }
}

...

// ----- Change Movements by Manipulating States -----

private IEnumerator MoveDown() {
    if (isMovingDownValid) {
        state = State.movingDown;
        yield return new WaitForSeconds(0.75f); // give 0.75s position translation time
        state = State.stop;

        ...
    }
}

private IEnumerator MoveUp() {
    if (!isMovingDownValid) {
        state = State.movingUp;
        yield return new WaitForSeconds(0.75f);
        state = State.stop;

        ...
    }
}

Tip

when moving either up and down, the whale will keep in moving state for 0.75s duration and then switch to stop posture.

In order to prevent the whale from moving downwards when it’s already low, or upwards when it’s already surfaced, a boolean property of isMovingDownValid has been used to check the currnet altitude and constraint the movement of the whale idle only upwards when it’s in lower altitude, and only downwards when it’s in upper altitude.

// Whale.js (... represents other code blocks irrelevant to the current session)

...

private IEnumerator MoveDown() {
    if (isMovingDownValid) {
        state = State.movingDown;
        yield return new WaitForSeconds(0.75f); // give 0.75s position translation time
        state = State.stop;

        // banning the whale from moving further downwards when it's already in lower position
        isMovingDownValid = false;
    }
}

private IEnumerator MoveUp() {
    if (!isMovingDownValid) {
        state = State.movingUp;
        yield return new WaitForSeconds(0.75f);
        state = State.stop;

        // banning the whale from moving further upwards when it's already in higher position
        isMovingDownValid = true;
    }
}

Splash

The splash manipulation has been implemented in a way animations and box colliders of the splash object are pre-defined but hidden as default when the splash has not been triggered:

../_images/Splash.jpg
// SplashManager.cs (... represents other code blocks irrelevant to the current session)

...

void Start() {
    ...

    // initially disable the box collider, animator and sprite render and trigger later
    box2D         = GetComponent<BoxCollider2D>();
    box2D.enabled = false;

    animator         = GetComponent<Animator>();
    animator.enabled = false;

    spriteRenderer         = GetComponent<SpriteRenderer>();
    spriteRenderer.enabled = false;

    // initially set the splash activatable to true
    isSplashActivatable = true;
}

...

When the button connected to Arduino has been pressed, all 3 components above will be set to true and thus make usable basically by calling ActivateSplash() method:

void ActivateSplash() {
    box2D.enabled          = true;
    animator.enabled       = true;
    spriteRenderer.enabled = true;
    Invoke("DeactivateSplash", splashDuration);
}

Note

Since the button stays at state of 1 during being pressed, this state will trigger multiple splashs in a row during the pressing. Therefore, a logic has to be implemented to allow only one splash within 0.5s by setting isSplashActivatable to false immediately after each splash:

public class SplashManager : MonoBehaviour {

    [SerializeField] private float splashDuration = 0.5f;
    private int buttonPressed = 0;

    private bool isSplashActivatable;

    ...

    void Start() {
        ...

        // initially set the splash activatable to true
        isSplashActivatable = true;
    }

    ...

    // ------- Button Control -------

    void ButtonControlSplash() {
        if (buttonPressed == 1) {
            ActivateSplash();

            PreventMultipleSplash();
        }
    }

    // ------- Enable and Disable Splash Activatable to mitigate splash overlay -------

    void PreventMultipleSplash() {
        // prevent the user from splashing various times within short time
        isSplashActivatable = false;

        // set the splash activatable property back to true after a short delay
        Invoke("SplashActivatable", 0.5f);
    }

    void SplashActivatable() {
        isSplashActivatable = true;
    }

    // ------- Splash Manipulations -------

    void ActivateSplash() {
        box2D.enabled          = true;
        animator.enabled       = true;
        spriteRenderer.enabled = true;
        Invoke("DeactivateSplash", splashDuration);
    }

    void DeactivateSplash() {
        box2D.enabled          = false;
        animator.enabled       = false;
        spriteRenderer.enabled = false;
    }
}

Lastly, the splash can only happen when the whale is surfaced. The information whether the whale is surfaced or not can be retrieved from the Whale class:

// SplashManager.cs (... represents other code blocks irrelevant to the current session)

[SerializeField] private GameObject whaleGameObject;
private Whale whale;

...

void Start() {
    ...

    whale = whaleGameObject.GetComponent<Whale>();

    ...
}

void Update() {
    ...

    // determine whether the whale altitude and only trigger at higher position
    if (whale.isMovingDownValid && isSplashActivatable) {
        KeyboardControlSplash();
        ButtonControlSplash();
    }
}

Tip

Using the property of isMovingDownValid of Whale class to determine the altitude level of the Whale, if it is true, that means the whale is surfaced and thus Splash is valid