Clucker

Lead Scripter| Game Designer |Project Manager

Genre: Puzzle Game

Engine: Unity

Language: C#

Development time: 3 weeks

Team: 3 game designers + 3 2D artist

Lead Scripter

  • Script puzzle logic and rules
  • Script player interactions
  • Script feedback systems

Project Manager

  • Organize team
  • Maintaining and ensuring game vision

Clucker is a puzzle game for mobile platforms and PC aimed toward casual gamers of all ages. Deliver eggs from the adorable chickens on the top to the baskets below by dragging and swapping the pipes around. The longer path you can create the more points you will get!

Cluckers delivers a unique puzzle experience that is quick and easy to grasp but opens up to more deep mechanics and puzzle challenges. With a simple engaging core mechanic, Clucker has a huge potential for variation, different game modes, and special levels that always gives players challenges and an incentive to return and play the game.

Lead scripter

As lead scripter in this project, I learned a lot about developing for mobile platforms, such as thinking about and considering both restrictions and possibilities on performance and touch interfaces and taking in consideration garbage collection and memory usage when developing by using design patterns like pooling and singletons to save performance and memory.

Project Manager

As project manager on this project, I learned a lot about working with people’s skills and utilize the strengths within the team. Help team members to deliver the best possible work during a limited amount of time. We had to constantly prioritize tasks and make quick decisions on what the game needed to grow into the best it could be.

Drag and drop system

The main interaction in the game is the dragging, dropping and swapping of pipe pieces around on the board. I implemented this feature by making use of Unity’s event system.

This resulted in a system that could be used both for mouse and touch interaction on both mobile platforms and PC and that could handle moving multiple pieces at once.

Show code implementation of DragablePiece in C#
using System;
using System.Linq;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using EggCollector.Utils;

[RequireComponent(typeof(BoxCollider2D))]
[RequireComponent(typeof(SpriteRenderer))]
public class DragablePiece : MonoBehaviour, IDragHandler, IBeginDragHandler, IEndDragHandler  {

    [Header("Slot")]
    public DragableSlot DragSlot;
    [Header("Drag Type")]
    public DragableType Type = DragableType.None;

    [Header("Movement")]
    public float Speed = 10;
    public float CloseDistance = 0.01f;
    public float SearchRadius = 0.25f;

    [Header("Sound")]
    public AudioClip SlotSwapSound;

    [Header("Can be draged")]
    public bool CanBeDraged = true;

    protected SpriteRenderer spRenderer;
    protected int originalSortingOrder;

    protected bool drag;
    public bool IsDraging
    {
        get { return drag; }
    }

    protected DragableSlot DragableHover;

    protected virtual void Start()
    {
        spRenderer = GetComponent<SpriteRenderer>();
        originalSortingOrder = spRenderer.sortingOrder;
    }

    protected virtual void Update()
    {
        //If not draging, move the piece towards it slot.
        if (!drag && DragSlot != null )
        {
            //if within clsoe distance, stop moving and set position.
            if (Vector3.Distance(DragSlot.transform.position, transform.position) > CloseDistance)
            {
                transform.position = Vector3.Lerp(transform.position, DragSlot.transform.position, Time.deltaTime * Speed);
                spRenderer.sortingOrder = Settings.DragSortLayer;
            }
            else
            {
                transform.position = DragSlot.transform.position;
                spRenderer.sortingOrder = originalSortingOrder;
            }
        }
    }

    public virtual void OnBeginDrag(PointerEventData eventData)
    {
        if (PlayManager.Instance.CanMove && CanBeDraged)
        {
            drag = true;
            spRenderer.sortingOrder = Settings.DragSortLayer;
            if(DragSlot != null)
            {
                DragSlot.DragableSlotChange();
            }
        }
    }

    public virtual void OnDrag(PointerEventData eventData)
    {
        if (PlayManager.Instance.CanMove && drag)
        {
            var delta = eventData.delta;
            var move = delta / TouchSystem.ScreenToUnitsDvition;
            transform.Translate(move);
        }
    }

    public virtual void OnEndDrag(PointerEventData eventData)
    {
        if (drag)
        {
            var closetsSlot = GetClosestDragable();
            if (closetsSlot != null && closetsSlot.CanBeMovedTo)
            {
                if (closetsSlot != null && closetsSlot.CanBeMovedTo && closetsSlot.Allowed(Type))
                {
                    SwapPice(closetsSlot);
                }
            }
            drag = false;
            if (DragSlot != null)
            {
                DragSlot.DragableSlotChange();
            }
        }
    }
    /// <summary>
    /// Makes a overlapp circle and returns the cloests slot within that circle.
    /// </summary>
    /// <returns>The closest slot within the search radius, returns null if none was found.</returns>
    protected DragableSlot GetClosestDragable()
    {
        var hits = Physics2D.OverlapCircleAll(transform.position, SearchRadius, LayerMask.GetMask("DragableSlot"));
        return hits.Where(x => x.gameObject.GetComponent<DragableSlot>() != null).OrderBy(x => Vector3.Distance(x.transform.position, transform.position)).Select(x => x.GetComponent<DragableSlot>()).FirstOrDefault();
    }

    /// <summary>
    /// Swaps the slots for this pice and the pice in the new slot.
    /// </summary>
    /// <param name="newSlot">The new slot to swap to</param>
    public void SwapPice(DragableSlot newSlot)
    {
        if (newSlot.CanBeMovedTo)
        {
            GlobalSoundController.Instance.PlaySound(SlotSwapSound);
            var oldSlot = this.DragSlot;
            var otherPice = newSlot.Piece;

            if (otherPice != null)
            {
                otherPice.DragSlot = oldSlot;
                otherPice.DragSlot.Piece = otherPice;
            }
            else if (oldSlot != null)
            {
                oldSlot.Piece = null;
            }

            newSlot.Piece = this;
            DragSlot = newSlot;
        }
    }

}

Show code implementation of DragableSlot in C#
using System.Linq;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using EggCollector.Utils;
using EggCollector.Interfaces;

[RequireComponent(typeof(BoxCollider2D))]
public class DragableSlot : MonoBehaviour
{
    public delegate void DragableSlotChangeEventHandler();
    public DragableSlotChangeEventHandler DragableSlotChangeEvent;

    public DragableType[] AllowedTypes;
    public DragablePiece Piece;

    public bool CanBeMovedTo
    {
        get
        {
            if(Piece != null)
            {
                return Piece.CanBeDraged;
            }
            return true;
        }
    }

    public bool PiceLifted
    {
        get
        {
            if (Piece == null)
            {
                return true;
            }

            return false;
        }
    }

    public bool Allowed(DragableType type)
    {
        return AllowedTypes.Contains(type);
    }

    public void DragableSlotChange()
    {
        if (DragableSlotChangeEvent != null)
        {
            DragableSlotChangeEvent();
        }
    }
}

Poolable objects

Garbage collection for Unity on mobile platforms can be performance heavy. Instantiating and destroying Game Objects quickly becomes a performance problem. To avoid this problem I decided to use pooling for all resources of the game.

Instead of instantiating and destroying objects I created a system for objects to be instantiated at the start of the game and added to a pool. These objects would then be activated and removed from the pool, or deactivated and re-added to the pool as needed.

This system saved a lot of performance, and is used for almost all elements of the game:

  • Pipes
  • Chickens
  • Baskets
  • Eggs
  • Play area grid
  • Score text
  • FX
Show code implementation of ObjectPooler in C#
using System.Linq;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using EggCollector.Interfaces;

public class ObjectPooler: MonoBehaviour {

    public GameObject Prefab;
    public int PoolChunkSize = 16;
    public bool InitOnStart = true;
    public bool AutoGrow = true;

    protected int currentPoolSize = 0;

    protected Stack<GameObject> DeactivatedObjects;
    public int CurrentPoolSize
    {
        get
        {
            return currentPoolSize;
        }
    }



    public int NumberOfActiveObjects
    {
        get
        {
            return currentPoolSize - DeactivatedObjects.Count();
        }
    }

    public int NumberOfDeactivatedObjects
    {
        get
        {
            return DeactivatedObjects.Count();
        }
    }

    protected virtual void Start()
    {
        DeactivatedObjects = new Stack<GameObject>();
        if (InitOnStart)
        {
            AddChunkToPool();
        }

    }

    protected virtual void AddChunkToPool()
    {
        for (int i = 0; i < PoolChunkSize; i++)
        {
            var go = Instantiate(Prefab, transform.position, Quaternion.identity, transform);
            go.SetActive(false);
            DeactivatedObjects.Push(go);
        }
        currentPoolSize += PoolChunkSize;
    }

    public GameObject GetObjectFromPool(Vector3 position, Quaternion rotation)
    {  
        if (DeactivatedObjects == null)
        {
            DeactivatedObjects = new Stack<GameObject>();
        }

        if (DeactivatedObjects.Count == 0 && AutoGrow)
        {
            AddChunkToPool();
        }

        if(DeactivatedObjects.Count > 0)
        {
            var go = DeactivatedObjects.Pop();

            go.transform.position = position;
            go.transform.rotation = rotation;
            go.SetActive(true);

            foreach (IPoolableObject poolScript in go.GetComponents<IPoolableObject>())
            {
                poolScript.ActivatedFromPool(position, rotation, this);
            }

            return go;
        }

        return null;
    }

    public void ReturnToPool(GameObject go)
    {
        go.SetActive(false);

        foreach (IPoolableObject poolScript in go.GetComponents<IPoolableObject>())
        {
            poolScript.DeactivateToPool();
        }
        DeactivatedObjects.Push(go);
    }


}
Show code implementation of IPoolableObject in C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEngine;

namespace EggCollector.Interfaces
{
    public interface IPoolableObject
    {
        void ActivatedFromPool(Vector3 postion, Quaternion rotation, ObjectPooler pool);
        void ReturnToPool();
        void DeactivateToPool();
    }
}
Show code of implementing PoolableObjects in C#
using System.Linq;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using EggCollector.Utils;
using EggCollector.Interfaces;

/// <summary>
/// This class shows an example implementation of the IPoolableObject interface.
/// </summary>
public class PoolableObject : MonoBehaviour, IPoolableObject
{

    /* >> IPoolableObject << */
    #region IPoolableObject
    protected ObjectPooler objectPooler;
    public virtual void ActivatedFromPool(Vector3 postion, Quaternion rotation, ObjectPooler pool)
    {
        objectPooler = pool;
        return;
    }

    public virtual void ReturnToPool()
    {
        objectPooler.ReturnToPool(this.gameObject);
    }

    public virtual void DeactivateToPool()
    {
        return;
    }
    #endregion
}

Level and grid stucture

In the game, levels are not separate scenes. Instead, each level is a set of configurations for the play area. The play area (the grid) receives this level configuration and generates the level based on the values of that level.

The grid can be configured to be of any size and have chickens and baskets of any color.

This allowed us to easily and quickly create and test new levels. It also means that we don’t need to load new scenes when the game is playing, eliminating all loading times completely.

Show code implementation of Levels in C#
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using EggCollector.Utils;

[CreateAssetMenuAttribute]
public class Level : ScriptableObject
{
    [System.Serializable]
    public struct LevelPice
    {
        public IntVector2 Position;
        public PipeDirectionTypes Direction;
        public PipeStatusTypes Type;
    }

    [System.Serializable]
    public struct ColorPlacement
    {
        public int Column;
        public HenColors Color;
    }

    [System.Serializable]
    public struct MedalRequirements
    {
        public int Bronze;
        public int Silver;
        public int Gold;
        public int MaxScore;
    }


    [System.Serializable]
    public struct LevelMessage
    {
        public bool ShowMessage;
        public string MessageHeader;
        public string MessageText;
        public string ButtonText;
    }

    [Header("Info")]
    public string LevelName;
    public LevelMessage Message;

    [Header("Eggs")]
    public int Eggs;

    [Header("Medals")]
    public MedalRequirements Goals;

    [Header("Size")]
    public IntVector2 Size;

    [Header("Chickens")]
    public ColorPlacement[] Chickens;
    [Header("Chicken Excaustion Time")]
    public int ChickenExcaustionTime = 3;

    [Header("Baskets")]
    public ColorPlacement[] Baskets;

    [Header("Start Pieces")]
    public LevelPice[] StartPieces;

    [Header("New pieces")]
    public bool EndlessPieces = true;
    public PipeDirectionTypes[] NewPiecs;

}
Show code implementation of Grid in C#
using System;
using System.Linq;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using EggCollector.Utils;
using EggCollector.Interfaces;
public class Grid : MonoBehaviour {

    [Serializable]
    public enum Pivot { Middle, BottomMiddle, BottomRight }

    [Header("Pivot")]
    public Pivot PivotPoint = Pivot.Middle;

    [Header("Dimentions")]
    public float HorizontalDistance = 0.5f;
    public float VerticalDistance = 0.5f;

    [Header("Poolers")]
    public ObjectPooler DragableSlotPool;
    public ObjectPooler GridIntersectionPool;
    public ObjectPooler ChickenPool;
    public ObjectPooler BasketPool;
    public CombinedPipePooler PipePool;

    /*  
    * Public members regarding sound and interfaces cut from this example.
    */

    private IntVector2 gridSize;
    public IntVector2 GridSize
    {
        get { return gridSize; }
    }
    public int GridWidth
    {
        get { return gridSize.x; }
    }
    public int GridHeight
    {
        get { return gridSize.y; }
    }
    public int NumberOfSlots
    {
        get { return GridWidth * GridHeight;  }
    }

    [HideInInspector]
    public DragableSlot[,] GridMatrix;
    [HideInInspector]
    public GirdIntersection[,] GridIntersections;
    [HideInInspector]
    public ChickenController[] Chickens;
    [HideInInspector]
    public BasketController[] Baskets;

    public float Width
    {
        get
        {
            return (GridWidth-1) * HorizontalDistance;
        }
    }

    public float Heigh
    {
        get
        {
            return (GridHeight-1) * VerticalDistance;
        }
    }

    /*  
    * Functions regardin initialization, start and update cut from this example.
    */


    public void SetUpLevel (Level newLevel, Action newLevelSetupCallback = null)
    {
        gridEmpty = false;

        level = newLevel;
        gridSize = level.Size;
        LevelSetUpCallback = newLevelSetupCallback;
    
        //Spawn Slots on each place in the grid.
        startOffset = GetStartOffset();
        var positionOffset = new Vector3();
        GridMatrix = new DragableSlot[GridWidth,GridHeight];
        for (int j = 0; j < GridHeight; j++)
        {
            positionOffset.x = 0;
            for (int i = 0; i < GridWidth; i++)
            {
                var pos = transform.position + startOffset + positionOffset;
                var slot = DragableSlotPool.GetObjectFromPool(pos, Quaternion.identity).GetComponent<DragableSlot>();
                slot.DragableSlotChangeEvent += GridUpdateEventHandler;

                GridMatrix[i, j] = slot;
                positionOffset.x += HorizontalDistance;
            }
            positionOffset.y += VerticalDistance;
        }


        //Spawn chicken and baskets for each column in the grid.
        var chickenOffset = new Vector3(0, GridHeight * VerticalDistance, 0) + ChickenPlacementOffset;
        var basketOffset = new Vector3(0, - VerticalDistance, 0);
        Chickens = new ChickenController[GridWidth];
        Baskets = new BasketController[GridWidth];
        for (int i = 0; i < GridWidth; i++)
        {
            //Chickens
            if(level.Chickens.Any(x => x.Column == i))
            {
                var chickenPos = transform.position + startOffset + chickenOffset;
                var chicken = ChickenPool.GetObjectFromPool(chickenPos, Quaternion.identity).GetComponent<ChickenController>();
                chicken.SetUpChickens(i, level.ChickenExcaustionTime, level.Chickens.FirstOrDefault(x => x.Column == i).Color);
                Chickens[i] = chicken;
            }
            chickenOffset.x += HorizontalDistance;

            //Baskets
            if (level.Baskets.Any(x => x.Column == i))
            {
                var basketPos = transform.position + startOffset + basketOffset;
                var basket = BasketPool.GetObjectFromPool(basketPos, Quaternion.identity).GetComponent<BasketController>();
                basket.SetUpBasket(i, level.Baskets.FirstOrDefault(x => x.Column == i).Color);
                Baskets[i] = basket;
            }
            basketOffset.x += HorizontalDistance;
        }

        //Spawn intersections on each intersection of the grid. (used by items, etc)
        Vector3 intersectionOffset = new Vector3(HorizontalDistance / 2f, VerticalDistance / 2f, 0);
        GridIntersections = new GirdIntersection[GridWidth - 1, GridHeight - 1];
        for (int j = 0; j < GridHeight-1; j++)
        {
            intersectionOffset.x = HorizontalDistance / 2f;
            for (int i = 0; i < GridWidth-1; i++)
            {
                var pos = intersectionOffset +transform.position + startOffset;
                var inter = GridIntersectionPool.GetObjectFromPool(pos, Quaternion.identity).GetComponent<GirdIntersection>();
                GridIntersections[i, j] = inter;
                intersectionOffset.x += HorizontalDistance;
            }

            intersectionOffset.y += VerticalDistance;
        }

        //Start spawning pipes.
        StartCoroutine(SpawnInitPipesInGrid());
    }


    private Vector3 GetStartOffset()
    {
        Vector3 startOffset;
        switch (PivotPoint)
        {
            case Pivot.Middle:
                startOffset = new Vector3(-Width / 2f, -Heigh / 2f, 0);
                break;
            case Pivot.BottomMiddle:
                startOffset = new Vector3(-Width / 2f, VerticalDistance / 2f, 0);
                break;
            case Pivot.BottomRight:
            default:
                startOffset = new Vector3(HorizontalDistance / 2f, VerticalDistance / 2f, 0);
                break;
        }
        return startOffset;
    }

    /* 
    * Helper functions and other functionality cut from this example
    */
}