Featherfall

Featherfall is a project that I and 11 others worked on in 7 weeks using the Unity engine at Future games. FeatherFall plays like an ability-based beat them up where you traverse the Greenland tundra. I mainly worked on the player character but also touched on the AI.

AI

AI system that caused the first Enemies to arrive to surround the player up close while all the others are to encircle around its target when there is no space up close.


public class MoveAround : MonoBehaviour
{
    public int segments = 10;
    public float innerRadius = 2;
    public float outerRadiusMin = 1.5f;
    public float outerRadiusMax = 10f;

    float deltaTheta;
    Vector3 targetLocation;
    
    bool isInPlayMode = false;

    float rotationTimer;
    public List<GameObject> enemy = new List<GameObject>();

    private void Start()
    {
        targetLocation = transform.position;
        deltaTheta = (2 * Mathf.PI) / segments;
        isInPlayMode = true;
    }

    private void Update()
    {
        rotationTimer += Time.deltaTime;
        targetLocation = transform.position;
        
        for(int i = 0; i < enemy.Count; i++)
        {
            if (i < segments)
            {
                SendAiToInnerPositionDependentOnIndex(i);
            }
            else
                SendAiToOuterPositionDependantOnIndex(i);
        }
    }

    Vector3 GetPositionAroundPlayer(int index, float extraRadius, bool shouldRotate = false)
    {
        float theta = deltaTheta * index;

        if (!shouldRotate)
            return targetLocation + new Vector3((innerRadius + extraRadius) * 
                Mathf.Cos(theta),
                0f,
                (innerRadius + extraRadius) * Mathf.Sin(theta));
        else
            return targetLocation + new Vector3((innerRadius + extraRadius) * 
                Mathf.Cos(rotationTimer + index) * Random.Range(outerRadiusMin, outerRadiusMax),
                0f, 
                (innerRadius + extraRadius) * Mathf.Sin(rotationTimer + index) * Random.Range(outerRadiusMin, outerRadiusMax));
    }
    
    void SendAiToPosition()
    {
        for(int i = 0;i < segments;i++)
        {
            float smalestdistance = Mathf.Infinity;
            Vector3 target = Vector3.zero;
            

            if (!enemy[i].activeInHierarchy)
            {
                GameObject tmp = enemy[i];
                enemy[i] = enemy[enemy.Count - 1];
                enemy[enemy.Count - 1] = tmp;
                enemy.RemoveAt(enemy.Count - 1);
            }

            for (int y = 0; y < segments; y++)
            {
                if (!enemy[i])
                    continue;

                Vector3 distance = enemy[i].transform.position - GetPositionAroundPlayer(i,0);

                if(distance.sqrMagnitude < smalestdistance)
                {
                    smalestdistance = distance.sqrMagnitude;
                    target = GetPositionAroundPlayer(i, 0);
                }
            }
            
            enemy[i].GetComponent<UnityEngine.AI.NavMeshAgent>().SetDestination(target);
        }
        if(segments < enemy.Count)
        {
            for(int i = segments; i < enemy.Count; i++)
            {
                enemy[i].GetComponent<UnityEngine.AI.NavMeshAgent>().SetDestination(GetPositionAroundPlayer(i-segments,0,true));
            }
        }
    }
    void SendAiToOuterPositionDependantOnIndex(int index)
    {
        enemy[index].GetComponent<UnityEngine.AI.NavMeshAgent>().SetDestination(GetPositionAroundPlayer(index - segments, 0, true));
    }

    void SendAiToInnerPositionDependentOnIndex(int index)
    {
        float smalestdistance = Mathf.Infinity;
        Vector3 target = Vector3.zero;

        int targetIndex = 0;

        if (!enemy[index].activeInHierarchy)
        {
            GameObject tmp = enemy[index];
            enemy[index] = enemy[enemy.Count - 1];
            enemy[enemy.Count - 1] = tmp;
            enemy.RemoveAt(enemy.Count - 1);
        }

        for (int y = 0; y < segments; y++)
        {
            if (!enemy[index])
                continue;

            Vector3 distance = enemy[index].transform.position - GetPositionAroundPlayer(index, 0);

            if (distance.sqrMagnitude < smalestdistance)
            {
                smalestdistance = distance.sqrMagnitude;
                target = GetPositionAroundPlayer(index, 0);
                targetIndex = y;
            }
        }

        enemy[index].GetComponent<UnityEngine.AI.NavMeshAgent>().SetDestination(target);
          
    }

    void CircularPos()
    {
        float theta = 0;
        for(int i = 0; i < enemy.Count; i++)
        {
            Vector3 segPos = targetLocation + new Vector3(innerRadius * Mathf.Cos(theta), 0f, innerRadius * Mathf.Sin(theta));
            enemy[i].GetComponent<UnityEngine.AI.NavMeshAgent>().SetDestination(segPos);
            theta += deltaTheta;
        }
    }

    private void OnDrawGizmos()
    {
        if (!isInPlayMode)
            return;
        for(int i = 0; i < segments; i++)
        {
            Gizmos.color = Color.red;
            Gizmos.DrawSphere(GetPositionAroundPlayer(i,0),0.2f);

        }
        Gizmos.color = Color.blue;
		Gizmos.DrawLine(gameObject.transform.position + 
		new Vector3(0, 0, innerRadius*outerRadiusMin), gameObject.transform.position + 
		new Vector3(0, 0, innerRadius * outerRadiusMax));
    }
}
								

I also made an Ai resource gathering behavior that makes them look for food, water and a place to rest. When they had gathered enough, they wander randomly. This got cut before the combat was implemented as we moved the game in a different direction.

When I was working on the Resource gathering, I also experimented a lot with behavior trees in unity using their animator as a stand-in for the fact that they don't have an actual behavior tree. This was something I later used in the project to help and apply animations in the game.


public bool CanSeePlayer = false;


    //Placeholder point of interest
    [Tooltip("Position for resource")] public Transform nestPosition;
    [Tooltip("Position for resource")] public Transform foodPosition;
    [Tooltip("Position for resource")] public Transform waterPosition;

    private UnityEngine.AI.NavMeshAgent navMeshAgent;
    private Animator animator;
    private Vector3 targetPosition;
    private float currentDistance;

    [Tooltip("max distance for random walking")]public float distance;

    [Tooltip("The time it takes for all the meters to decrease by one")]public float timer = 15f;
    private float startTimer;
    
    [Tooltip("animal internal resources, used in determining behavior")] [Range(1, 10)] public int sleepMeter = 10;
    [Tooltip("animal internal resources, used in determining behavior")] [Range(1, 10)] public int foodMeter = 10;
    [Tooltip("animal internal resources, used in determining behavior")] [Range(1, 10)] public int waterMeter = 10;

    [System.NonSerialized]public IdleState animalState = IdleState.Hungry;
    
    public enum IdleState  
    {
        None,
        Hungry,
        Thirsty,
        Sleepy,
    };

    public void SetState(IdleState state)
    {
        if (sleepMeter == 10 && foodMeter == 10 && waterMeter == 10)
        {
            animalState = IdleState.None;
            return;
        }
        else
            animalState = state;
    }

    public IdleState CheckState()
    {
        switch (animalState)
        {
            case IdleState.Hungry:
                if (foodMeter != 10)
                    break;
                else if (waterMeter < 5)
                    animalState = IdleState.Thirsty;
                else if (sleepMeter < 5)
                    animalState = IdleState.Sleepy;
                else
                    animalState = IdleState.None;
                break;
            case IdleState.Thirsty:
                if (waterMeter != 10)
                    break;
                else if (foodMeter < 5)
                    animalState = IdleState.Hungry;
                else if (sleepMeter < 5)
                    animalState = IdleState.Sleepy;
                else
                    animalState = IdleState.None;
                break;
            case IdleState.Sleepy:
                if (sleepMeter != 10)
                    break;
                else if (waterMeter < 5)
                    animalState = IdleState.Thirsty;
                else if (foodMeter < 5)
                    animalState = IdleState.Hungry;
                else
                    animalState = IdleState.None;
                break;
            case IdleState.None:
                if (foodMeter < 5)
                    animalState = IdleState.Hungry;
                else if (sleepMeter < 5)
                    animalState = IdleState.Sleepy;
                else if (waterMeter <5)
                    animalState = IdleState.Thirsty;
                else
                    animalState = IdleState.None;
                break;
        }
        return animalState;
    }

    private void Awake()
    {
        animator = gameObject.GetComponent<Animator>();
        navMeshAgent = gameObject.GetComponent<UnityEngine.AI.NavMeshAgent>();

        
    }
    
    void FixedUpdate()
    {
        // decramenting resources
        startTimer += Time.deltaTime;
        if(startTimer > timer)
        {
            foodMeter--;
            waterMeter--;
            sleepMeter--;

           foodMeter = Mathf.Clamp(foodMeter, 1, 10);
           waterMeter = Mathf.Clamp(waterMeter, 1, 10);
           sleepMeter = Mathf.Clamp(sleepMeter, 1, 10);

            startTimer = 0f;
        }

        // check idle state distance
        switch(CheckState())
        {
            case IdleState.Hungry:
                targetPosition = foodPosition.position;
                break;
            case IdleState.Sleepy:
                targetPosition = nestPosition.position;
                break;
            case IdleState.Thirsty:
                targetPosition = waterPosition.position;
                break;
            default:
                break;
        }
        currentDistance = Vector3.Distance(targetPosition, transform.position);
        animator.SetFloat("DistanceToObjectiv", currentDistance);
    }

    public static Vector3 RandomNavSphere(Vector3 origin, float dist, int layermask)
    {
        Vector3 randDirection = Random.insideUnitSphere * dist;

        randDirection += origin;

        NavMeshHit navHit;

        NavMesh.SamplePosition(randDirection, out navHit, dist, layermask);

        return navHit.position;
    }

    public void GoToIdlePoint()
    {
        navMeshAgent.SetDestination(targetPosition);
    }

    public void GoToRandomPoint()
    {
        navMeshAgent.SetDestination(RandomNavSphere(transform.position,distance,-1));
    }
								

Camera

I made two Camera functions. A Lock on that would follow a target and allowed for cycling in between them, and a zoom. This was added to the game when the team first thought it was necessary, but was later removed in favor of big swiping attacks that felt better gameplay-wise.

void Update()
    {
        if(Input.GetMouseButtonDown(2))
        {
            isZoomed = !isZoomed;
        }
        if(Input.GetMouseButtonUp(2))
        {
            isZoomed = false;
        }

        if (isZoomed)
        {
            GetComponent<Camera>().fieldOfView = Mathf.Lerp(GetComponent<Camera>().fieldOfView, zoom, Time.deltaTime * smoth);
        }
        else
        {
            GetComponent<Camera>().fieldOfView = Mathf.Lerp(GetComponent<Camera>().fieldOfView, startZoom, Time.deltaTime * smoth);
        }

        if (Input.GetKeyDown(KeyCode.Space))
        {
            isInTargetmode = !isInTargetmode;
        }

        if (Input.GetKeyDown(KeyCode.LeftArrow))
        {
            isLookingForNewTarget = true;
            toggleEnemyDir = 1;
        }

        if (Input.GetKeyDown(KeyCode.RightArrow))
        {
            isLookingForNewTarget = true;
            toggleEnemyDir = -1;
        }

        if(isInTargetmode)
        {
            //check what enemies that are in range
            if(LockOnTargets.Count == 0)
            {
                Collider[] colArrr = Physics.OverlapSphere(transform.position, range, lockOnLayer);
                LockOnTargets = new List<Collider>(colArrr);
            }
            else
            {
                Collider[] colArr = Physics.OverlapSphere(transform.position, range, lockOnLayer); 
                foreach(Collider c in colArr)
                {
                    if (!LockOnTargets.Contains(c))
                        LockOnTargets.Add(c);
                }
                LockOnTargets.RemoveAll(item => !new List<Collider>(colArr).Contains(item));
            }

            // lock on to closest one
            if (LockOnTargets.Count > 0)
            {
                if(!targetEnemy)
                {
                    targetEnemy = LockOnTargets[0];
                    foreach (Collider c in LockOnTargets)
                    {
                        if (Vector3.Distance(c.transform.position, transform.position) <
                            Vector3.Distance(targetEnemy.transform.position, transform.position))
                        {
                            targetEnemy = c;
                        }
                    }
                }

                //cycle target to lock on to another "random" target
                if (isLookingForNewTarget && LockOnTargets.Count >1)
                {
                    LockOnTargets.Remove(targetEnemy);
                    float dotComparing = 0f;
                    Collider nextTargetEnemy = null;
                    for(int i = 0; i< LockOnTargets.Count; i++)
                    {
                        if (LockOnTargets[i] == targetEnemy)
                            continue;

                        Vector3 currentLookDir = transform.forward;
                        Vector3 nextLookDir = (transform.position - LockOnTargets[i].transform.position).normalized;
                        Vector3 rightVector = transform.right * toggleEnemyDir;


                        if(Vector3.Dot(rightVector, nextLookDir) > 0)
                        {
                            float enemyDotResult = Vector3.Dot(currentLookDir, nextLookDir);
                            if (enemyDotResult < dotComparing)
                            {
                                dotComparing = enemyDotResult;
                                nextTargetEnemy = LockOnTargets[i];
                            }
                                
                        }
                    }
                    LockOnTargets.Add(targetEnemy);
                    targetEnemy = nextTargetEnemy;
                    isLookingForNewTarget = false;
                }
            }
            //adjust camera toward target
            relPos = targetEnemy.transform.position - transform.position;
            newRot = Quaternion.LookRotation(relPos);
            transform.rotation = Quaternion.RotateTowards(transform.rotation, newRot, Time.deltaTime * speed);
        }
        else
        {
            if(LockOnTargets != null)
                LockOnTargets.Clear();
            targetEnemy = null;
        }
    }

    private void OnDrawGizmosSelected()
    {
        Gizmos.color = Color.red;
        Gizmos.DrawWireSphere(transform.position, range);
        if(targetEnemy)
        {
            Gizmos.color = Color.blue;
            Gizmos.DrawLine(transform.position, transform.position + transform.right * toggleEnemyDir);

            Gizmos.color = Color.green;
            Gizmos.DrawLine(targetEnemy.transform.position, targetEnemy.transform.position + (targetEnemy.transform.position - targetEnemy.transform.position));
        }
    }
}
                                            
>

What I learned

The biggest problem we had was that we started the project badly. We didn’t start with an initial prototype, the player was not finished first and a lot of the starting power was put on the enemies instead. This caused the starting prosses of making the player’s movement and combat feel good to happen way too late in the project. So what I learned is to focus on the things that are most commonly encountered in the game and make sure that those parts feel good before moving on.