Porting Lepz 3rd platform sample to C#

Hi all,

I ported Lerpz 3D Plateform sample to c# to allow me to reuse some of it in my own project. All of it was succesfully ported and behave as expected.

The only problem i’m facing now is the EnemyPoliceGuy.js script not behaving as it shoulds. EnemyPoliceGuy.js is the main controller for enemies and it manages a state-machine and acts upon it to make them alive.

I’m sure the problem lies in the usage i’m making of the yield statement. Sadly, i’ve never used the yield statement in my previous c# projects.

So here’s my translation to c# of the famous EnemyPoliceGuy.js script. :wink:

using UnityEngine;
using System.Collections;

/*
	animations played are:
	idle, threaten, turnjump, attackrun
*/
[RequireComponent(typeof(AudioSource))]
[RequireComponent(typeof(CharacterController))]
public class EnemyPoliceGuy : MonoBehaviour
{
    public float attackTurnTime = 0.7f;
    public float rotateSpeed = 120.0f;
    public float attackDistance = 17.0f;
    public float extraRunTime = 2.0f;
    public float damage = 1.0f;

    public float attackSpeed = 5.0f;
    public float attackRotateSpeed = 20.0f;

    public float idleTime = 1.6f;

    public Vector3 punchPosition = new Vector3(0.4f, 0, 0.7f);
    public float punchRadius = 1.1f;

    // sounds
    public AudioClip idleSound;	// played during "idle" state.
    public AudioClip attackSound;	// played during the seek and attack modes.

    private float attackAngle = 10.0f;
    private bool isAttacking = false;
    private float lastPunchTime = 0.0f;

    public Transform target;

    // Cache a reference to the controller
    private CharacterController characterController;

    // Cache a link to LevelStatus state machine script:
    public LevelStatus levelStateMachine;

    public void Awake()
    {
        characterController = GetComponent<CharacterController>();

        levelStateMachine = GameObject.Find("/Level").GetComponent<LevelStatus>();

        if (!levelStateMachine)
        {
            Debug.Log("EnemyPoliceGuy: ERROR! NO LEVEL STATUS SCRIPT FOUND.");
        }
    }

    public IEnumerator Start()
    {

        if (!target)
            target = GameObject.FindWithTag("Player").transform;

        if (animation)
        {
            animation.wrapMode = WrapMode.Loop;

            // Setup animations
            animation.Play("idle");
            animation["threaten"].wrapMode = WrapMode.Once;
            animation["turnjump"].wrapMode = WrapMode.Once;
            animation["gothit"].wrapMode = WrapMode.Once;
            animation["gothit"].layer = 1;
        }

        // initialize audio clip. Make sure it's set to the "idle" sound.
        audio.clip = idleSound;

        yield return new WaitForSeconds(Random.value);

        // Just attack for now
        while (true)
        {
            // Don't do anything when idle. And wait for player to be in range!
            // This is the perfect time for the player to attack us
            yield return Idle();

            // Prepare, turn to player and attack him
            yield return Attack();
        }
    }


    public IEnumerator Idle()
    {
        // if idling sound isn't already set up, set it and start it playing.
        if (idleSound)
        {
            if (audio.clip != idleSound)
            {
                audio.Stop();
                audio.clip = idleSound;
                audio.loop = true;
                audio.Play();	// play the idle sound.
            }
        }

        // Don't do anything when idle
        // The perfect time for the player to attack us
        yield return new WaitForSeconds(idleTime);

        // And if the player is really far away.
        // We just idle around until he comes back
        // unless we're dying, in which case we just keep idling.
        while (true)
        {
            characterController.SimpleMove(Vector3.zero);
            yield return new WaitForSeconds(0.2f);

            var offset = transform.position - target.position;

            // if player is in range again, stop lazyness
            // Good Hunting!		
            if (offset.magnitude < attackDistance)
                yield return null;
        }
    }

    public float RotateTowardsPosition(Vector3 targetPos, float rotateSpeed)
    {
        // Compute relative point and get the angle towards it
        var relative = transform.InverseTransformPoint(targetPos);
        var angle = Mathf.Atan2(relative.x, relative.z) * Mathf.Rad2Deg;
        // Clamp it with the max rotation speed
        var maxRotation = rotateSpeed * Time.deltaTime;
        var clampedAngle = Mathf.Clamp(angle, -maxRotation, maxRotation);
        // Rotate
        transform.Rotate(0, clampedAngle, 0);
        // Return the current angle
        return angle;
    }

    public IEnumerator Attack()
    {
        isAttacking = true;

        if (attackSound)
        {
            if (audio.clip != attackSound)
            {
                audio.Stop();	// stop the idling audio so we can switch out the audio clip.
                audio.clip = attackSound;
                audio.loop = true;	// change the clip, then play
                audio.Play();
            }
        }

        // Already queue up the attack run animation but set it's blend wieght to 0
        // it gets blended in later
        // it is looping so it will keep playing until we stop it.
        animation.Play("attackrun");

        // First we wait for a bit so the player can prepare while we turn around
        // As we near an angle of 0, we will begin to move
        float angle;
        angle = 180.0f;
        float time;
        time = 0.0f;
        Vector3 direction;
        while (angle > 5 || time < attackTurnTime)
        {
            time += Time.deltaTime;
            angle = Mathf.Abs(RotateTowardsPosition(target.position, rotateSpeed));
            var move = Mathf.Clamp01((90 - angle) / 90);

            // depending on the angle, start moving
            animation["attackrun"].weight = animation["attackrun"].speed = move;
            direction = transform.TransformDirection(Vector3.forward * attackSpeed * move);
            characterController.Move(direction);

            yield return null;
        }

        // Run towards player
        var timer = 0.0f;
        var lostSight = false;
        while (timer < extraRunTime)
        {
            angle = RotateTowardsPosition(target.position, attackRotateSpeed);

            // The angle of our forward direction and the player position is larger than 50 degrees
            // That means he is out of sight
            if (Mathf.Abs(angle) > 40)
                lostSight = true;

            // If we lost sight then we keep running for some more time (extraRunTime). 
            // then stop attacking 
            if (lostSight)
                timer += Time.deltaTime;

            // Just move forward at constant speed
            direction = transform.TransformDirection(Vector3.forward * attackSpeed);
            characterController.SimpleMove(direction);

            // Keep looking if we are hitting our target
            // If we are, knock them out of the way dealing damage
            var pos = transform.TransformPoint(punchPosition);
            if (Time.time > lastPunchTime + 0.3  (pos - target.position).magnitude < punchRadius)
            {
                // deal damage
                target.SendMessage("ApplyDamage", damage);
                // knock the player back and to the side
                var slamDirection = transform.InverseTransformDirection(target.position - transform.position);
                slamDirection.y = 0;
                slamDirection.z = 1;
                if (slamDirection.x >= 0)
                    slamDirection.x = 1;
                else
                    slamDirection.x = -1;
                target.SendMessage("Slam", transform.TransformDirection(slamDirection));
                lastPunchTime = Time.time;
            }

            // We are not actually moving forward.
            // This probably means we ran into a wall or something. Stop attacking the player.
            if (characterController.velocity.magnitude < attackSpeed * 0.3)
                break;

            // yield for one frame
            yield return null;
        }

        isAttacking = false;

        // Now we can go back to playing the idle animation
        animation.CrossFade("idle");
    }

    public void ApplyDamage()
    {
        animation.CrossFade("gothit");
    }

    public void OnDrawGizmosSelected()
    {
        Gizmos.color = Color.yellow;
        Gizmos.DrawWireSphere(transform.TransformPoint(punchPosition), punchRadius);
        Gizmos.color = Color.red;
        Gizmos.DrawWireSphere(transform.position, attackDistance);
    }
}

To my knowledge, im pretty sure the mistake is in the Start method, where the yield statement is used to maintain and allow state changes.

Any inputs would be appreciated,
Thanks!

This is a great question. I’ve tried this other ways with methods that return IEnumerations, and waiting for them. But it seems redundant and clumsy.

Please change from

            // Don't do anything when idle. And wait for player to be in range!
            // This is the perfect time for the player to attack us
            yield return Idle(); 

            // Prepare, turn to player and attack him
            yield return Attack();

to

            // Don't do anything when idle. And wait for player to be in range!
            // This is the perfect time for the player to attack us
            yield return StartCoroutine(Idle());

            // Prepare, turn to player and attack him
            yield return StartCoroutine(Attack());

It works well.