Chef Line's Journey

Work In Progress

Food Themed Action Adventure Game


Description

This project originated from my first internship at a company called EmberGlitch. I had the unique opportunity to lead my own project with a dedicated team of six members. Throughout this experience, we received valuable guidance and support from the professionals at EmberGlitch.

The premise of the game is that you play as a chef who has been transported to a food-themed world. In this world, you must fight or cook your way through various challenges to uncover the mysteries of this strange place and understand why you were brought here.

Chef Line's Journey Playthrough Teaser


Team

This team consisted of nearly the same members as in my previous project, Starline. Consequently, teamwork was quite seamless, and I encountered absolutely no difficulties working with these fantastic individuals.

Team Members: 6

    Developers
  • Leandro Bonora (Programming)
  • Milan Driece (Programming)

  • Artists
  • Aiden Glen (2D & 3D)
  • Anna Heijblom (3D, Animator)
  • Jordy Gouw (3D Prop Artist)
  • Jae Gerritsen (3D, VFX)

Work Process

Since this project was quite different from the usual, it was particularly interesting for me. I decided to take on the role of Scrum Master once again, actively working to improve my team leadership skills alongside my mentors at EmberGlitch. We placed a strong emphasis on learning from professionals, which resulted in thorough documentation, planning, and discussions within the team before diving into development.

I worked on various aspects of the project, including enemy design and behavior, as well as several smaller features. I handled most of the UI-related tasks and implemented interactive elements, health systems, and more.


I also contributed significantly to the extensive Game Design Document 30+ Pages we created before commencing development. Feel free to check it out!



Enemy Behavior

For the enemy behavior in our game, I implemented a system based on state machines. This approach allowed me to efficiently manage and control various states such as idle, roaming, chasing, attacking, and fleeing. Each state was designed to handle specific conditions and transitions, ensuring that the enemies behaved dynamically and realistically within the game environment. Taking this approach was a nice challenge because previously, I had implemented almost everything in the same script, Recognizing that my previous approach, which consolidated everything into a single script, was not ideal, I transitioned to state machines. This decision enabled me to modularize and organize enemy behaviors more effectively, making the code more readable and maintainable for my fellow developer. The resulting flexible framework not only enhanced gameplay immersion but also increased amount of effort needed for making new enemies.


Im really happy with how everything turned out. Switching to state machines made it so much easier for me to add different types of enemies and reuse stuff effectively. It streamlined the whole process and made the enemies more flexible. Now, I can quickly tweak and adjust enemy behaviors without a lot of hassle.


Underneath, you can see one of the small critters I made aswell as the StateMachine and State Scripts I made that serve as the base for the critter states

GIF Description
GIF Description



                
                    public class StateMachine : State
                    {
                        [SerializeField] protected State _currentState;
                    
                        public void GoToState()
                        {
                            GoToState(typeof(T));
                        }
                    
                        public void GoToState(Type type)
                        {
                            if (_currentState != null)
                            {
                                _currentState.Deactivate();
                            }
                    
                            State newState = GetComponentInChildren(type) as State;
                            if (newState == null)
                            {
                                GameObject go = new GameObject(type.Name);
                                go.transform.SetParent(transform);
                                newState = go.AddComponent(type) as State;
                            }
                    
                            newState.Machine = this;
                            newState.Activate();
                            _currentState = newState;
                        }
                    
                        private void Update()
                        {
                            if (_currentState != null)
                            {
                                _currentState.ActiveUpdate();
                            }
                        }
                    }
                
            
                
                    public class State : MonoBehaviour
                    {
                        public StateMachine Machine { get; set; }
                    
                        public void Activate()
                        {
                            OnActivate();
                        }
                    
                        public void ActiveUpdate()
                        {
                            OnActiveUpdate();
                        }
                    
                        public void Deactivate()
                        {
                            OnDeactivate();
                        }
                    
                        protected virtual void OnActivate()
                        {
                    
                        }
                    
                        protected virtual void OnActiveUpdate()
                        {
                    
                        }
                    
                        protected virtual void OnDeactivate()
                        {
                    
                        }
                    }
                
            

Critter Behavior

For critter behavior, I made the following scripts managing various states for their interactions in the game environment. The foundational class supporting these scripts is the BaseEnemyState, integrating components such as Field of View (FOV) and serving as the basis for specific state scripts: CritterAttackState, EnemyIdleState, EnemyRoamState, and EnemyChaseState.


The EnemyRoamState script manages the roaming behavior of AI enemies within the game environment. This script is integral to controlling how enemies navigate and explore their surroundings, enhancing the game's realism and challenge. The script allows enemies to either patrol predefined points (_fixedRoamPoints) or dynamically explore new areas based on environmental conditions. Critters adjust their roaming patterns based on game events and player proximity, making their behavior more responsive and engaging. I added in Debugging options to be able to quickly check why something is not working and help troubleshooting.


 
                              
public class EnemyRoamState : BaseEnemyState
{
    [Header("Fixed Roam Settings")]
    [SerializeField] protected bool _useFixedRoamPoints = false;
    [SerializeField] protected Vector3[] _fixedRoamPoints;

    protected CapsuleCollider _capsuleCollider;
    protected Vector3 _walkPoint;
    protected bool _walkPointSet;

    private int _currentPatrolIndex = 0;
    private int _maxRecursionDepth = 20;

    protected override void OnActivate()
    {
        if (_capsuleCollider == null) _capsuleCollider = Machine.GetComponent();
    }

    protected override void OnActiveUpdate()
    {
        FindTargetsInView();
        HandleRoaming();

        ToggleHealthBar(false);
        if (Machine._anim != null) Machine._anim.SetFloat("WalkSpeed", Machine._agent.velocity.magnitude);

        if (Machine._targetsInView.Count > 0)
        {
            Machine.GoToState();
        }

        if (Machine._currentRoamTime >= Machine._maxTimeTillIdle && Machine._ableToGoBackToIdle)
        {
            Machine._currentRoamTime = 0;
            _walkPoint = Vector3.zero;
            _walkPointSet = false;
            Machine.GoToState();
        }
    }

    private void HandleRoaming()
    {
        Machine._currentRoamTime += Time.deltaTime;

        if (Vector3.Distance(transform.position, Machine._startPoint) > Machine._maxDistanceFromStartPoint)
        {
            TurnBackToStart();
            return;
        }

        if (!Machine._lastLocationSet)
        {
            Roaming();
        }
        else
        {
            Machine._agent.SetDestination(_walkPoint = Machine.LastTargetPosition);
            Vector3 distanceToWalkPoint = transform.position - _walkPoint;

            if (distanceToWalkPoint.magnitude < 1.25f)
            {
                _walkPointSet = false;
                Machine._lastLocationSet = false;
            }
        }
    }

    protected void Roaming()
    {
        Machine._agent.isStopped = false;

        if (_useFixedRoamPoints && _fixedRoamPoints.Length > 0)
        {
            Machine._agent.SetDestination(_fixedRoamPoints[_currentPatrolIndex]);

            if (Vector3.Distance(transform.position, _fixedRoamPoints[_currentPatrolIndex]) < 1f)
                _currentPatrolIndex = (_currentPatrolIndex + 1) % _fixedRoamPoints.Length;
        }
        else if (!_useFixedRoamPoints)
        {
            if (!_walkPointSet)
                SearchWalkPoint();

            if (_walkPointSet)
                Machine._agent.SetDestination(_walkPoint);

            Vector3 distanceToWalkPoint = transform.position - _walkPoint;

            if (distanceToWalkPoint.magnitude < 1.25f)
                _walkPointSet = false;
        }
    }

    protected void SearchWalkPoint(int recursionDepth = 0)
    {
        if (recursionDepth >= _maxRecursionDepth)
        {
            if (Machine._enableDebugLogs) Debug.LogWarning("Max recursion depth reached. Unable to find a walk point.");
            TurnAround();
            return;
        }

        Vector3 randomPoint = RandomPointInViewCone(transform.position, Machine._FOV._viewAngle, Machine._FOV._viewRadius);

        if (Vector3.Distance(randomPoint, Machine._startPoint) > Machine._maxDistanceFromStartPoint)
        {
            if (Machine._enableDebugLogs) Debug.Log($"Recursion Depth: {recursionDepth}, Distance from Start point too great");
            SearchWalkPoint(recursionDepth + 1);
            return;
        }

        RaycastHit hitInfo;
        if (Physics.Raycast(randomPoint + Vector3.up * 100f, Vector3.down, out hitInfo, Mathf.Infinity, Machine._groundLayerMask))
        {
            randomPoint.y = hitInfo.point.y + _capsuleCollider.height * 0.5f;
        }
        else
        {
            if (Machine._enableDebugLogs) Debug.Log($"Recursion Depth: {recursionDepth}, Failed to find ground for Y position");
            SearchWalkPoint(recursionDepth + 1);
            return;
        }

        NavMeshHit navMeshHit;
        if (!NavMesh.SamplePosition(randomPoint, out navMeshHit, 1.0f, NavMesh.AllAreas))
        {
            if (Machine._enableDebugLogs) Debug.Log($"Recursion Depth: {recursionDepth}, Point not on NavMesh");
            SearchWalkPoint(recursionDepth + 1);
            return;
        }

        if (Physics.Raycast(transform.position, randomPoint - transform.position, out hitInfo, Machine._attackRange, Machine._obstacleMask))
        {
            if (hitInfo.collider != null)
            {
                if (Machine._enableDebugLogs) Debug.Log($"Recursion Depth: {recursionDepth}, Obstacle detected in front");
                SearchWalkPoint(recursionDepth + 1);
                return;
            }
        }

        if (randomPoint.y > Machine.transform.position.y + 0.5f)
        {
            if (Machine._enableDebugLogs) Debug.Log($"Recursion Depth: {recursionDepth}, Point too high");
            SearchWalkPoint(recursionDepth + 1);
            return;
        }

        _walkPoint = randomPoint;
        _walkPointSet = true;
        if (Machine._enableDebugLogs) Debug.Log($"Walk point found: {_walkPoint}, Recursion Depth: {recursionDepth}");
    }

    protected Vector3 RandomPointInViewCone(Vector3 origin, float angle, float radius)
    {
        Quaternion currentRotation = transform.rotation;
        float randomAngle = Random.Range(-angle / 2f, angle / 2f);
        Quaternion randomRotation = currentRotation * Quaternion.Euler(0f, randomAngle, 0f);
        Vector3 randomDirection = randomRotation * Vector3.forward;
        Vector3 randomPoint = origin + randomDirection * radius;
        return randomPoint;
    }

    protected void TurnAround()
    {
        RaycastHit hitInfo;
        Vector3 oppositeDirection = -transform.forward;
        _walkPoint = transform.position + oppositeDirection * Machine._FOV._viewRadius;

        if (Physics.Raycast(transform.position, oppositeDirection, out hitInfo, Machine._FOV._viewRadius, Machine._obstacleMask))
        {
            if (hitInfo.collider != null)
            {
                _walkPointSet = true;
                if (Machine._enableDebugLogs) Debug.Log("Turned Around");
                return;
            }
        }
        _walkPointSet = true;
    }

    private void TurnBackToStart()
    {
        _walkPoint = Machine._startPoint;
        Machine._agent.SetDestination(_walkPoint);
        _walkPointSet = true;
        _walkPoint = Vector3.zero;

        _walkPointSet = false;
        SearchWalkPoint();
    }
}
                
            


Reflection

This project was unlike any other I've worked on in several ways. Our approach was notably different; instead of diving straight into development, we took quite some time and crafted a comprehensive Game Design Document spanning over 40 pages. This method was a shift from my usual trial-and-error approach, but it proved invaluable in guiding our development process.

Transitioning from a school environment to an office setting was a refreshing change of pace, albeit it took some adjustment initially. Fortunately, I quickly adapted to my new surroundings and embraced the professional atmosphere.

Reflecting on this project, I realize how much I've grown, both professionally and personally. Beyond developing technical skills, I've learned the importance of empathy and collaboration. This experience has really cemented the realization of the value of being a supportive team member, which has had a positive impact on both my work and relationships with people in general.

Image Description