Search Unity

Limits to the usefulness of a FSM character controller?

Discussion in 'Scripting' started by TheProfessor, May 22, 2015.

  1. TheProfessor

    TheProfessor

    Joined:
    Nov 2, 2014
    Posts:
    74
    As the title says I'm wondering if I've hit a wall as to where a FSM can be legitimately useful for controller a 2D sprite in a game (or a 3D character) or if my implementation is just shoddy? I was following the "2D Unity Game Development" book and it suggested I use a FSM for controlling my character; this worked well enough for jumping, left/right movement and other things, but when I tried to expand upon it for a Mario sort of game, and make it handle more complex controls I kept having issue after issue: Such as states being overridden by the controller for no reason, for example while walking the character would suddenly switch to idle for no reason I could debug; or massive increases in complexity as I had to keep trying to solidly pin down the logic of how certain actions would work. The largest difficulty being having a mixture of two states, say moving and jumping or moving while falling and so on. These got more and more troublesome.

    Here's my controller:
    Code (csharp):
    1.  
    2.     float mDuration = 0.0f;
    3.     float mMxDuration = 5.0f;
    4.     float mSpeed = 2.0f;
    5.     bool gLeft;
    6.     // Use this for initialization
    7.     void Start () {
    8.         gLeft = true;
    9.         mDuration = mMxDuration + Time.time;
    10.     }
    11.    
    12.     // Update is called once per frame
    13.     void Update () {
    14.         if (gLeft)
    15.         {
    16.             transform.Translate(new Vector3((mSpeed * -1.0f) * Time.deltaTime, 0.0f, 0.0f));
    17.         }
    18.         else
    19.         {
    20.             transform.Translate(new Vector3((mSpeed * 1.0f) * Time.deltaTime, 0.0f, 0.0f));
    21.         }
    22.  
    23.        if (gLeft && (mDuration < Time.time))
    24.         {
    25.             gLeft = false;
    26.             mDuration = mMxDuration + Time.time;
    27.         }
    28.         else if (!gLeft && (mDuration < Time.time))
    29.         {
    30.             gLeft = true;
    31.             mDuration = mMxDuration + Time.time;
    32.         }
    33.     }
    34.  
    And here's my listener:
    Code (csharp):
    1.  
    2.    public float playerWalkSpeed = 3f;
    3.     public Vector2 velocity;
    4.     public static float playerJumpForceVertical = 600f;
    5.     public static float playerJumpForceHorizontal = 100f;
    6.     public static float playerMoveForceHorizontal = 4.350f;
    7.     public static float playerRunningForceHorizontal = 6.550f;
    8.     private bool playerHasLanded = true;
    9.     private Animator playerAnimator = null;
    10.     private PlayerStateController.playerStates currentState = PlayerStateController.playerStates.idle;
    11.     private PlayerStateController.playerStates previousState = PlayerStateController.playerStates.idle;
    12.     private Rigidbody2D rb;
    13.     private float currentHeightPos;
    14.     private float prevHeightPos;
    15.     private float animLength = 2.15f;
    16.     private float runTimer = -1.0f;
    17.     private float runTimerDuration = 1.0f;
    18.  
    19.     void Start ()
    20.     {
    21.         rb = GetComponent<Rigidbody2D>();
    22.         playerAnimator = GetComponent<Animator>();
    23.         rigidbody2D.fixedAngle = true;
    24.         PlayerStateController.stateDelayTimer[(int)PlayerStateController.playerStates.jump] = 1.0f;
    25.         //PlayerStateController.stateDelayTimer[(int)PlayerStateController.playerStates.idle] = 0.1f;
    26.         currentHeightPos = prevHeightPos = transform.position.y;
    27.     }
    28.     void OnEnable()
    29.     {
    30.         PlayerStateController.onStateChange += onStateChange;
    31.     }
    32.  
    33.     void OnDisable()
    34.     {
    35.         PlayerStateController.onStateChange -= onStateChange;
    36.     }
    37.     // use this for initialization
    38.  
    39.  
    40.     void onStateCycle()
    41.     {
    42.         // Grab the current localScale of the object so we have
    43.         // access to it in the following code
    44.         Vector3 localScale = transform.localScale;
    45.  
    46.         transform.localEulerAngles = Vector3.zero;
    47.         //Debug.Log("Current State: " + currentState);
    48.         //Debug.Log("Idle timer: " + PlayerStateController.stateDelayTimer[(int)PlayerStateController.playerStates.idle]);
    49.         switch (currentState)
    50.         {
    51.            
    52.             case PlayerStateController.playerStates.idle:
    53.                 //rigidbody2D.velocity = new Vector2(0, rigidbody2D.velocity.y);
    54.                 //Debug.Log("Current State: Idle");
    55.                 if (playerHasLanded && (prevHeightPos > currentHeightPos))
    56.                 {
    57.                     //Debug.Log("Idle Falling");
    58.                     //onStateChange(PlayerStateController.playerStates.falling);
    59.                 }
    60.                 break;
    61.  
    62.             case PlayerStateController.playerStates.left:
    63.                 //transform.Translate(new Vector3((playerWalkSpeed * -1.0f) * Time.deltaTime, 0.0f, 0.0f));
    64.                 //Debug.Log("Current State: left");
    65.                 if ((runTimer < Time.time) && runTimer != -1.0f)
    66.                 {
    67.                     Debug.Log("Current timer: " + runTimer);
    68.                     onStateChange(PlayerStateController.playerStates.running_left);
    69.                 }
    70.                 else
    71.                 {
    72.                     if (playerHasLanded)
    73.                     {
    74.                         //rb.MovePosition(rb.position - (velocity + new Vector2(3, 0)) * Time.deltaTime);
    75.                         rigidbody2D.velocity = new Vector2(-playerMoveForceHorizontal, rigidbody2D.velocity.y);
    76.                         if (localScale.x > 0.0f)
    77.                         {
    78.                             localScale.x *= -1.0f;
    79.                             transform.localScale = localScale;
    80.                         }
    81.                     }
    82.                     else
    83.                     {
    84.                         //rb.velocity = new Vector2(-1*playerMoveForceHorizontal, 0);
    85.                         rigidbody2D.AddForce(new Vector2(-1 * (playerMoveForceHorizontal), 0));
    86.                     }
    87.  
    88.                     if (prevHeightPos > currentHeightPos)
    89.                     {
    90.                         //Debug.Log("Here Left");
    91.                         onStateChange(PlayerStateController.playerStates.falling);
    92.                     }
    93.                 }
    94.                 break;
    95.             case PlayerStateController.playerStates.right:
    96.                 //transform.Translate(new Vector3(playerWalkSpeed * Time.deltaTime, 0.0f, 0.0f));
    97.                 //Debug.Log("Current State: Right");
    98.                 if ((runTimer < Time.time) && runTimer != -1.0f)
    99.                 {
    100.                     Debug.Log("Current timer: " + runTimer);
    101.                     onStateChange(PlayerStateController.playerStates.running_right);
    102.                 }
    103.                 else
    104.                 {
    105.                     if (playerHasLanded)
    106.                     {
    107.                         //rb.MovePosition(rb.position + (velocity + new Vector2(3,0)) * Time.deltaTime);
    108.                         rigidbody2D.velocity = new Vector2(playerMoveForceHorizontal, rigidbody2D.velocity.y);
    109.  
    110.                         if (localScale.x < 0.0f)
    111.                         {
    112.                             localScale.x *= -1.0f;
    113.                             transform.localScale = localScale;
    114.                         }
    115.                     }
    116.                     else
    117.                     {
    118.                         //rb.velocity = new Vector2(1 * playerMoveForceHorizontal, 0);
    119.                         rigidbody2D.AddForce(new Vector2(1 * (playerMoveForceHorizontal), 0));
    120.                     }
    121.  
    122.                     if (prevHeightPos > currentHeightPos)
    123.                     {
    124.                         //Debug.Log("Here Right");
    125.                         onStateChange(PlayerStateController.playerStates.falling);
    126.                     }
    127.                 }
    128.                 break;
    129.             case PlayerStateController.playerStates.jump:
    130.                 /*
    131.                  * If the current state is jumping, we need to test if we're falling, and if so, change state to
    132.                  * falling.
    133.                  */
    134.                 if (prevHeightPos > currentHeightPos)
    135.                 {
    136.                     onStateChange(PlayerStateController.playerStates.falling);
    137.                 }
    138.                 break;
    139.             case PlayerStateController.playerStates.landing:
    140.                 if (previousState == currentState)
    141.                 {
    142.                     onStateChange(PlayerStateController.playerStates.falling);
    143.                 }
    144.                 break;
    145.             case PlayerStateController.playerStates.falling:
    146.                 //Debug.Log("Current State: Falling");
    147.                 break;
    148.             case PlayerStateController.playerStates.kill:
    149.                 //onStateChange(PlayerStateController.playerStates.resurrect);
    150.                 break;
    151.             case PlayerStateController.playerStates.resurrect:
    152.                 //onStateChange(PlayerStateController.playerStates.idle);
    153.                 break;
    154.             case PlayerStateController.playerStates.running_left:
    155.                 //Debug.Log("Current State: left");
    156.                 if (playerHasLanded)
    157.                 {
    158.                     //rb.MovePosition(rb.position - (velocity + new Vector2(3, 0)) * Time.deltaTime);
    159.                     rigidbody2D.velocity = new Vector2(-playerRunningForceHorizontal, rigidbody2D.velocity.y);
    160.                     if (localScale.x > 0.0f)
    161.                     {
    162.                         localScale.x *= -1.0f;
    163.                         transform.localScale = localScale;
    164.                     }
    165.                 }
    166.                 else
    167.                 {
    168.                     //rb.velocity = new Vector2(-1*playerMoveForceHorizontal, 0);
    169.                     rigidbody2D.AddForce(new Vector2(-1 * (playerMoveForceHorizontal), 0));
    170.                 }
    171.  
    172.                 if (prevHeightPos > currentHeightPos)
    173.                 {
    174.                     //Debug.Log("Here Left");
    175.                     onStateChange(PlayerStateController.playerStates.falling);
    176.                 }
    177.                 break;
    178.             case PlayerStateController.playerStates.running_right:
    179.                 //Debug.Log("Current State: left");
    180.                 if (playerHasLanded)
    181.                 {
    182.                     //rb.MovePosition(rb.position - (velocity + new Vector2(3, 0)) * Time.deltaTime);
    183.                     rigidbody2D.velocity = new Vector2(playerRunningForceHorizontal, rigidbody2D.velocity.y);
    184.                     if (localScale.x < 0.0f)
    185.                     {
    186.                         localScale.x *= -1.0f;
    187.                         transform.localScale = localScale;
    188.                     }
    189.                 }
    190.                 else
    191.                 {
    192.                     //rb.velocity = new Vector2(-1*playerMoveForceHorizontal, 0);
    193.                     rigidbody2D.AddForce(new Vector2(1 * (playerMoveForceHorizontal), 0));
    194.                 }
    195.  
    196.                 if (prevHeightPos > currentHeightPos)
    197.                 {
    198.                     //Debug.Log("Here Left");
    199.                     onStateChange(PlayerStateController.playerStates.falling);
    200.                 }
    201.                 break;
    202.             //case PlayerStateController.playerStates.firingWeapon:
    203.                 //break;
    204.         }
    205.         //Debug.Log("Current State " + currentState);
    206.     }
    207.  
    208.     // onStateChange is called whenever we make a change to the player's state
    209.     // from anywhere within the game's code.
    210.     public void onStateChange(PlayerStateController.playerStates newState)
    211.     {
    212.         // If the current state and the new state are the same, abort - no need
    213.         // to change to the state we're already in.
    214.         if (newState == currentState)
    215.             return;
    216.         // Verify there are no special conditions that would cause this state to abort
    217.         if (checkIfAbortOnStateCondition(newState))
    218.             return;
    219.         // Check if the current state is allowed to transition into this state. If it's not, abort.
    220.         if (!checkForValidStatePair(newState))
    221.             return;
    222.         //Debug.Log("Current State: "+ currentState);
    223.         // Having reached here, we now know that this state change is allowed.
    224.         // So let's perform the necessary actions depending on what the new state is.
    225.         switch (newState)
    226.         {
    227.             case PlayerStateController.playerStates.idle:
    228.                 Debug.Log("onStateChange: Idle");
    229.                 runTimer = runTimer = -1.0f;
    230.                 playerAnimator.SetBool("Jumping", false);
    231.                 playerAnimator.SetBool("Walking", false);
    232.                 playerAnimator.SetBool("Falling", false);
    233.                 playerAnimator.SetBool("Running", false);
    234.                 //PlayerStateController.stateDelayTimer[(int)PlayerStateController.playerStates.idle] = Time.time + 0.1f;
    235.                 PlayerStateController.stateDelayTimer[(int)PlayerStateController.playerStates.jump] = Time.time + 0.1f;
    236.                 break;
    237.             case PlayerStateController.playerStates.left:
    238.                 if (playerHasLanded)
    239.                 {
    240.                     Debug.Log("onStateChange: Walking Left");
    241.                     playerAnimator.SetBool("Walking", true);
    242.                     playerAnimator.SetBool("Running", false);
    243.                     runTimer = Time.time + runTimerDuration;
    244.                 }
    245.                 else
    246.                 {
    247.                     Debug.Log("onStateChange: Moving Left");
    248.                     rigidbody2D.AddForce(new Vector2(-1 * playerMoveForceHorizontal, 0));
    249.                 }
    250.                 break;
    251.             case PlayerStateController.playerStates.right:
    252.                 if (playerHasLanded)
    253.                 {
    254.                     Debug.Log("onStateChange: Walking Right");
    255.                     playerAnimator.SetBool("Walking", true);
    256.                     playerAnimator.SetBool("Running", false);
    257.                     runTimer = Time.time + runTimerDuration;
    258.                 }
    259.                 else
    260.                 {
    261.                     Debug.Log("onStateChange: Moving Right");
    262.                     rigidbody2D.AddForce(new Vector2(1 * playerMoveForceHorizontal, 0));
    263.                 }
    264.                 break;
    265.             case PlayerStateController.playerStates.jump:
    266.                 runTimer = -1.0f;
    267.                 if (playerHasLanded)
    268.                 {
    269.                     // Use the jumpDirection variable to specify if the player should be jumping left, right or vertical
    270.                     //runTimer = 0.0f;
    271.                     Debug.Log("onStateChange: Jumping");
    272.                     playerAnimator.SetBool("Jumping", true);
    273.                     playerAnimator.SetBool("Walking", false);
    274.                     playerAnimator.SetBool("Falling", false);
    275.                     playerAnimator.SetBool("Running", false);
    276.                     float jumpDirection = 0.0f;
    277.                     if (currentState == PlayerStateController.playerStates.left)
    278.                         jumpDirection = -1.0f;
    279.  
    280.                     else if (currentState == PlayerStateController.playerStates.right)
    281.                         jumpDirection = 1.0f;
    282.                     else
    283.                         jumpDirection = 0.0f;
    284.  
    285.                     // Apply the actual jump force
    286.                     rigidbody2D.AddForce(new Vector2(jumpDirection * playerJumpForceHorizontal, playerJumpForceVertical));
    287.  
    288.  
    289.                     playerHasLanded = false;
    290.                     PlayerStateController.stateDelayTimer[(int)PlayerStateController.playerStates.jump] = 0.0f;
    291.                 }
    292.                 break;
    293.             case PlayerStateController.playerStates.landing:
    294.                 Debug.Log("onStateChange: Landing");
    295.                 playerHasLanded = true;
    296.                 StartCoroutine(StandingUp());
    297.                 //PlayerStateController.stateDelayTimer[(int)PlayerStateController.playerStates.idle] = Time.time + 0.5f;
    298.                 break;
    299.             case PlayerStateController.playerStates.falling:
    300.                 Debug.Log("onStateChange: Falling");
    301.                 runTimer = -1.0f;
    302.                 playerHasLanded = false;
    303.                 playerAnimator.SetBool("Walking", false);
    304.                 playerAnimator.SetBool("Jumping", false);
    305.                 playerAnimator.SetBool("Falling", true);
    306.                 playerAnimator.SetBool("Running", false);
    307.                 PlayerStateController.stateDelayTimer[(int)PlayerStateController.playerStates.jump] = 0.0f;
    308.                // PlayerStateController.stateDelayTimer[(int)PlayerStateController.playerStates.idle] = 0.0f;
    309.                 break;
    310.             case PlayerStateController.playerStates.kill:
    311.                 break;
    312.             case PlayerStateController.playerStates.resurrect:
    313.                 //transform.position = playerRespawnPoint.transform.position;
    314.                // transform.rotation = Quaternion.identity;
    315.                // rigidbody2D.velocity = Vector2.zero;
    316.                 break;
    317.             case PlayerStateController.playerStates.running_left:
    318.                 Debug.Log("onStateChange: Running Left");
    319.                 playerAnimator.SetBool("Running", true);
    320.                 playerAnimator.SetBool("Jumping", false);
    321.                 playerAnimator.SetBool("Falling", false);
    322.                 break;
    323.             case PlayerStateController.playerStates.running_right:
    324.                 Debug.Log("onStateChange: Running Right");
    325.                 playerAnimator.SetBool("Running", true);
    326.                 playerAnimator.SetBool("Jumping", false);
    327.                 playerAnimator.SetBool("Falling", false);
    328.                 break;
    329.                 /*
    330.             case PlayerStateController.playerStates.firingWeapon:
    331.                 // Make the bullet object
    332.                 GameObject newBullet = (GameObject)Instantiate(bulletPrefab);
    333.  
    334.                 // Setup the bullet’s starting position
    335.                 newBullet.transform.position = bulletSpawnTransform.position;
    336.  
    337.                 // Acquire the PlayerBulletController component on the new object so we can specify some data
    338.                 PlayerBulletController bullCon = newBullet.GetComponent<PlayerBulletController>();
    339.  
    340.                 // Set the player object
    341.                 bullCon.playerObject = gameObject;
    342.  
    343.                 // Launch the bullet!
    344.                 bullCon.launchBullet();
    345.  
    346.                 // With the bullet made, set the state of the player back to the current state
    347.                 onStateChange(currentState);
    348.  
    349.                 PlayerStateController.stateDelayTimer[(int)PlayerStateController.playerStates.firingWeapon] = Time.time + 0.25f;
    350.                 break;
    351.                  * */
    352.         }
    353.  
    354.         // Store the current state as the previous state
    355.         previousState = currentState;
    356.  
    357.         // And finally, assign the new state to the player object
    358.         currentState = newState;
    359.         //Debug.Log("Current State " + currentState);
    360.  
    361.     }
    362.  
    363.     // Compare the desired new state against the current, and see if we are
    364.     // allowed to change to the new state. This is a powerful system that ensures
    365.     // we only allow the actions to occur that we want to occur.
    366.     bool checkForValidStatePair(PlayerStateController.playerStates newState)
    367.     {
    368.         bool returnVal = false;
    369.  
    370.         // Compare the current against the new desired state.
    371.         switch (currentState)
    372.         {
    373.             case PlayerStateController.playerStates.idle:
    374.                 // Any state can take over from idle.
    375.                 returnVal = true;
    376.                 break;
    377.             case PlayerStateController.playerStates.left:
    378.                 // Any state can take over from the player moving left.
    379.                 if (newState == PlayerStateController.playerStates.idle && !playerHasLanded)
    380.                     returnVal = false;
    381.                 else
    382.                     returnVal = true;
    383.                 break;
    384.             case PlayerStateController.playerStates.right:
    385.                 // Any state can take over from the player moving right.
    386.                 if (newState == PlayerStateController.playerStates.idle && !playerHasLanded)
    387.                     returnVal = false;
    388.                 else
    389.                     returnVal = true;
    390.                 break;
    391.             case PlayerStateController.playerStates.jump:
    392.                 // The only state that can take over from Jump is landing or kill.
    393.                 if (
    394.                     //newState == PlayerStateController.playerStates.landing
    395.                     newState == PlayerStateController.playerStates.falling
    396.                     || newState == PlayerStateController.playerStates.kill
    397.                     || newState == PlayerStateController.playerStates.left
    398.                     || newState == PlayerStateController.playerStates.right
    399.                     //|| newState == PlayerStateController.playerStates.firingWeapon
    400.                   )
    401.                     returnVal = true;
    402.                 else
    403.                     returnVal = false;
    404.                 break;
    405.             case PlayerStateController.playerStates.landing:
    406.                 // The only state that can take over from landing is idle, left or right movement.
    407.                 if (
    408.                     newState == PlayerStateController.playerStates.idle
    409.                     //||newState == PlayerStateController.playerStates.left
    410.                     //|| newState == PlayerStateController.playerStates.right
    411.                    
    412.                     //|| newState == PlayerStateController.playerStates.firingWeapon
    413.                   )
    414.                     returnVal = true;
    415.                 else
    416.                     returnVal = false;
    417.                 break;                
    418.             case PlayerStateController.playerStates.falling:
    419.                 // The only states that can take over from falling are landing or kill
    420.                 if (
    421.                     newState == PlayerStateController.playerStates.landing
    422.                     || newState == PlayerStateController.playerStates.kill
    423.                     || newState == PlayerStateController.playerStates.left
    424.                     || newState == PlayerStateController.playerStates.right
    425.                     //|| newState == PlayerStateController.playerStates.firingWeapon
    426.                   )
    427.                     returnVal = true;
    428.                 else
    429.                     returnVal = false;
    430.                 break;                
    431.             case PlayerStateController.playerStates.kill:
    432.                 // The only state that can take over from kill is resurrect
    433.                 if (newState == PlayerStateController.playerStates.resurrect)
    434.                     returnVal = true;
    435.                 else
    436.                     returnVal = false;
    437.                 break;
    438.             case PlayerStateController.playerStates.resurrect:
    439.                 // The only state that can take over from Resurrect is Idle
    440.                 if (newState == PlayerStateController.playerStates.idle)
    441.                     returnVal = true;
    442.                 else
    443.                     returnVal = false;
    444.                 break;
    445.             case PlayerStateController.playerStates.running_left:
    446.                 // Any state can take over from the player running.
    447.                 if (newState == PlayerStateController.playerStates.left)
    448.                     returnVal = false;
    449.                 else
    450.                     returnVal = true;
    451.                 break;
    452.             case PlayerStateController.playerStates.running_right:
    453.                 // Any state can take over from the player running.
    454.                 if (newState == PlayerStateController.playerStates.right)
    455.                     returnVal = false;
    456.                 else
    457.                     returnVal = true;
    458.                 break;
    459.                 /*
    460.             case PlayerStateController.playerStates.firingWeapon:
    461.                 returnVal = true;
    462.                 break;
    463.                  */
    464.         }
    465.         return returnVal;
    466.     }
    467.  
    468.     // checkIfAbortOnStateCondition allows us to do additional state verification, to see
    469.     // if there is any reason this state should not be allowed to begin.
    470.     bool checkIfAbortOnStateCondition(PlayerStateController.playerStates newState)
    471.     {
    472.         bool returnVal = false;
    473.  
    474.         switch (newState)
    475.         {
    476.             case PlayerStateController.playerStates.idle:
    477.                 break;
    478.             case PlayerStateController.playerStates.left:
    479.                 break;
    480.             case PlayerStateController.playerStates.right:
    481.                 break;                
    482.             case PlayerStateController.playerStates.jump:
    483.                 float nextAllowedJumpTime = PlayerStateController.stateDelayTimer[(int)PlayerStateController.playerStates.jump];
    484.  
    485.                 if (nextAllowedJumpTime == 0.0f || nextAllowedJumpTime > Time.time)
    486.                     returnVal = true;
    487.                 break;                
    488.             case PlayerStateController.playerStates.landing:
    489.                 break;                
    490.             case PlayerStateController.playerStates.falling:
    491.                 break;                
    492.             case PlayerStateController.playerStates.kill:
    493.                 break;
    494.             case PlayerStateController.playerStates.resurrect:
    495.                 break;
    496.             case PlayerStateController.playerStates.running_left:
    497.                 break;
    498.             case PlayerStateController.playerStates.running_right:
    499.                 break;
    500.                 /*
    501.             case PlayerStateController.playerStates.firingWeapon:
    502.                 if (PlayerStateController.stateDelayTimer[(int)PlayerStateController.playerStates.firingWeapon] > Time.time)
    503.                     returnVal = true;
    504.  
    505.                 break;
    506.                  */
    507.         }
    508.  
    509.         // Value of true means 'Abort'. Value of false means 'Continue'.
    510.         return returnVal;
    511.     }
    512.  
    513.     void Update()
    514.     {
    515.         prevHeightPos = currentHeightPos;
    516.         currentHeightPos = transform.position.y;
    517.     }
    518.     // Update is called once per frame.
    519.     void FixedUpdate()
    520.     {
    521.         onStateCycle();
    522.     }
    523.  
    524.     IEnumerator StandingUp()
    525.     {
    526.         yield return new WaitForSeconds(animLength);
    527.         // trigger the stop animation events here
    528.         playerAnimator.SetBool("Jumping", false);
    529.         playerAnimator.SetBool("Walking", false);
    530.         playerAnimator.SetBool("Falling", false);
    531.         //attacking = false;
    532.     }
    533.  
    As you can see I have a lot of debug output to try to pin down what goes wrong in what order!

    I think a major issue was edge cases, my means of determining "falling" was by if the character's y position changed from last frame and so on; but often had problems if I jumped and slide sideways onto the edge of a platform and so on. Very difficult to pin down what went wrong in edge cases.

    rigidbody2d's velocity/addforce/transform also gave me issues figuring out which one to use. Transform seemed to never result in my character randomly switching states or getting stuck (circle collider didn't help), but then would "bounce" from the edges of walls and obstacles which didn't fit how it works in Marion iirc; but using velocity was problematic, though sadly I don't remember why as this was a week ago when I handed in the assignment.

    So my question is; what are good best practices for using a FSM for a character controller for 2D and 3D characters?
     
  2. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,531
    These things don't really sound like fsm related, but instead just how you're dealing with the character itself.

    Like your rigidbody issue... it'd still be there if you used a FSM or not. In the end, you're going to have a bit of a time getting a Mario feel with the Rigidbody. Rigidbody (and Rigidbody2d) are for creating a realish physics feel... mario is NOT real physics by any stretch.
     
  3. Kiwasi

    Kiwasi

    Joined:
    Dec 5, 2013
    Posts:
    16,860
    One giant switch statement does not an FSM make. This is not a problem with the concept of a FSM, it's a problem with your implementation.

    Google round a bit for FSM frameworks in Unity. You'll find cleaner ways to implement state behaviour.

    There are limits to FSMs for sure, but you are a long way from hitting them.
     
  4. Iron-Warrior

    Iron-Warrior

    Joined:
    Nov 3, 2009
    Posts:
    838
    This is really the key. The largest benefit (in my experience) of using a state machine is how much easier it makes to debug your code, as well as how much cleaner it is. I recently used a FSM based off a series of tutorials from the now-defunct Unity gems to build a controller to replicate the controls from Super Mario 64...so it can definitely be used for fairly complex cases.

    Here's the state machine I used, as well as an example of how it's used here. Essentially you write methods like *_EnterState, *_ExitState, *_Update and so on, with the * being replaced by a state name and the script uses Reflection to dynamically discover your methods. It's pretty great.
     
    Lahcene, Ryiah and Kiwasi like this.
  5. TheProfessor

    TheProfessor

    Joined:
    Nov 2, 2014
    Posts:
    74
    Yeah I definitely felt there was some sort of implementation problem but wasn't sure *what*, the links given by Iron-Warrior are great and I hope to use them for the next assignment. :)
     
  6. RockoDyne

    RockoDyne

    Joined:
    Apr 10, 2014
    Posts:
    2,234
    It is definitely the worst method for making an FSM. Once you start needing entry and exit states, switch statements end up blowing up in size to the point where they are just unmanageable.
     
    Kiwasi likes this.
  7. TheProfessor

    TheProfessor

    Joined:
    Nov 2, 2014
    Posts:
    74
    I guess that's the downside to following textbook examples :)
     
  8. Kiwasi

    Kiwasi

    Joined:
    Dec 5, 2013
    Posts:
    16,860
    What a terrible text book. If this is how it suggests to do a FSM, I suggest you Google every other concept the book mentions just to do a sanity check. Its worth it.
     
    Ryiah likes this.
  9. TheProfessor

    TheProfessor

    Joined:
    Nov 2, 2014
    Posts:
    74
    Well I know for sure its out of date for GUI stuff, (all canvases now ja?) it's hard to nail down what other concepts it has that I need to research more into. RigidBody stuff for sure probably, it used transforms when apparently that's bad and should use addforce or velocity depending on what I'm trying to do. I imagine how it does AI is probably so simple it rolls over into being "wrong" in that it won't work for any other example.
     
  10. Kiwasi

    Kiwasi

    Joined:
    Dec 5, 2013
    Posts:
    16,860
    There is a difference between being out of date on UI stuff, that Unity just updated, and being out of date on something as old as a FSM. FSMs and FSM frameworks are far older then Unity, using a giant switch has been a bad idea since before Unity was even thought of.
     
  11. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,531
    I personally use a Component for each state (i call them movement styles), there is a component to control which state we're in (I call it the MovementMotor), and then some resolvers that can share stuff amongst the states (like a JumpResolver that multiple states will use as jump is the same).

    As you can see here:
    organized_entity.png
     
  12. TheProfessor

    TheProfessor

    Joined:
    Nov 2, 2014
    Posts:
    74
    Is there something special class or code that's missing? I copied and pasted it and I get a load of errors, like "Action".
     
  13. shadowninjapie

    shadowninjapie

    Joined:
    May 22, 2015
    Posts:
    9
    yeah unity is so old they dont have action. u need to make it urself.
     
  14. TheProfessor

    TheProfessor

    Joined:
    Nov 2, 2014
    Posts:
    74
    It's not really clear what Action is, is it something particular to C# in general?
     
  15. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,531
  16. Iron-Warrior

    Iron-Warrior

    Joined:
    Nov 3, 2009
    Posts:
    838
    I just pasted them into a new project in Unity 5 and then compile fine. Did you make sure that you have the using calls at the top of the script? Action and others belong to the System namespace and a number of things would show errors without it.

    ^Edit: beat me to it.
     
  17. TheProfessor

    TheProfessor

    Joined:
    Nov 2, 2014
    Posts:
    74
    That explains it! What's the difference between enum and Enum?
     
  18. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,531
    enum is the keyword for declaring a new enum type.

    Enum is the class System.Enum which is the type for enums.


    It's what System.Array is to all the various arrays like int[] and object[].
     
    Kiwasi likes this.
  19. TheProfessor

    TheProfessor

    Joined:
    Nov 2, 2014
    Posts:
    74
    So I'm poking at this, if I still want to segregate my player script to a control script that handles input and a listener script that handles the logic of what the player does in those states, I'd still use delegates/events to send information right? The general idea is not to use a giant switch statement for everything but to just generally think openly of a system that logically ping pongs between the different states?

    Instead of a "current" state, we have these special versions of fixed/late update instead that handles what to do when in a particular state?

    edit: Also I seem to be able to set currentState (Which is System.Enum) to an enum, but I can't seem to compare it, I assume .Equals works here?
     
    Last edited: May 24, 2015
  20. Iron-Warrior

    Iron-Warrior

    Joined:
    Nov 3, 2009
    Posts:
    838
    Yes. You inherit from the SimpleStateMachine with your player's actual logic, but other than that you are free to structure your scripts any way you like. If you want a more comprehensive demo on how it's used, you can download the Unity Example Project on my page here. This is a demo project for a custom character controller (the actually controller, not the player logic) but it has a basic player logic script included. The state machine script I use in that project is slightly modified to work with my custom character controller, but is the same in principal. I mainly bring it up since the demo project does segregate the player logic from the player input into separate scripts.
     
  21. TheProfessor

    TheProfessor

    Joined:
    Nov 2, 2014
    Posts:
    74
    Oh that's your blog!? That's amazing! I was just reading your blog at work earlier this week :D I'll be sure to look into it more later, for now though here's my current script as a simple example below, as I slowly adopt the code you linked me, and right now whenever I let go of a key it takes ~about a second for the state to change over to idle (but is immediate for states left/right):

    Code (csharp):
    1.  
    2.   [SerializeField]
    3.   float playerFlySpeed = 5.0f;
    4.  
    5.   void OnEnable()
    6.   {
    7.   bPlayerController.changeState += changeState;
    8.   }
    9.  
    10.   void OnDisable()
    11.   {
    12.   bPlayerController.changeState -= changeState;
    13.   }
    14.    // Use this for initialization
    15.    void Start() {
    16.   currentState = bPlayerController.playerStates.Idle;
    17.    }
    18.  
    19.   void changeState(bPlayerController.playerStates newState)
    20.   {
    21.  
    22.   // If the current state and the new state are the same, abort - no need
    23.   // to change to the state we're already in.
    24.   if (newState.Equals(currentState))
    25.   {
    26.   //Debug.Log("onStateChange: Duplicate: " + newState + ", return.");
    27.   //return;
    28.   }
    29.  
    30.   switch (newState)
    31.   {
    32.   case bPlayerController.playerStates.Idle:
    33.   Debug.Log("onStateChange: Idle");
    34.   currentState = bPlayerController.playerStates.Idle;
    35.   break;
    36.   case bPlayerController.playerStates.Left:
    37.   Debug.Log("onStateChange: Left");
    38.   currentState = bPlayerController.playerStates.Left;
    39.   break;
    40.   case bPlayerController.playerStates.Right:
    41.   Debug.Log("onStateChange: Right");
    42.   currentState = bPlayerController.playerStates.Right;
    43.   break;
    44.   }
    45.  
    46.   }
    47.  
    48.   // Updates
    49.   void Left_Update()
    50.   {
    51.   transform.Translate(new Vector3((playerFlySpeed * -1.0f) * Time.deltaTime, 0.0f, 0.0f));
    52.   }
    53.  
    54.   void Right_Update()
    55.   {
    56.   transform.Translate(new Vector3((playerFlySpeed * 1.0f) * Time.deltaTime, 0.0f, 0.0f));
    57.   }
    58.  
    59.   void Idle_Update()
    60.   {
    61.   Debug.Log("onStateChange: Idle");
    62.   // Do nothing!
    63.   }
    64.  
    Aaaand for some reason I didn't copy the right control script in my op, here it is:
    Code (csharp):
    1.  
    2.   public enum playerStates
    3.   {
    4.   Idle, Left, Right, Forward, Backward
    5.   }
    6.  
    7.   public delegate void playerStateHandler(bPlayerController.playerStates newState);
    8.  
    9.   public static event playerStateHandler changeState;
    10.  
    11.  
    12.    // Use this for initialization
    13.    void Start () {
    14.    
    15.    }
    16.    
    17.    // Update is called once per frame
    18.    void Update () {
    19.   float horizontal = Input.GetAxis("Horizontal");
    20.  
    21.   if (horizontal != 0.0f)
    22.   {
    23.   if (horizontal < 0.0f)
    24.   {
    25.   if (changeState != null)
    26.   changeState(bPlayerController.playerStates.Left);
    27.   }
    28.   else
    29.   {
    30.   if (changeState != null)
    31.   changeState(bPlayerController.playerStates.Right);
    32.   }
    33.   }
    34.   else
    35.   {
    36.   changeState(bPlayerController.playerStates.Idle);
    37.   }
    38.    }
    39.  
    It's immediate for switching between left/right but lags a bit for Idle. My current solution is probably to try detecting it via KeyUp. I ask here on the off chance its something about the FSM code that's throwing it off.
     
  22. TheProfessor

    TheProfessor

    Joined:
    Nov 2, 2014
    Posts:
    74
    I got it, its because GetAxis returns a value that "persists" for a bit in Unity that gradually goes to zero every frame, so for the second it isn't zero it stays in the left/right state before switching to idle. GetRawAxis seems to fix it but I there's probably better solutions.
     
  23. TheProfessor

    TheProfessor

    Joined:
    Nov 2, 2014
    Posts:
    74
    Alright I've started looking at your project and how did I not think of having the current control inputs be a variable that I pass to the script? Your way is way better.
     
  24. TheProfessor

    TheProfessor

    Joined:
    Nov 2, 2014
    Posts:
    74
    So suppose I'm working on a top-down shooter like Ikuruga, and don't need to check for being grounded or anything else the SuperCharacterController class does, what's the best way to bypass it? Right now I made an empty script put the sendMessage function in update.
     
  25. Iron-Warrior

    Iron-Warrior

    Joined:
    Nov 3, 2009
    Posts:
    838
    I wouldn't bother using the Super Character Controller unless you really need to customize the behavior of your character's collisions and whatnot. Just use the default CC with the SimpleStateMachine if you want FSM (or of course build your own FSM).