Notes and extensions of unity3d action game development practice 2.1

Time:2021-7-29

2.1.1. Use coprocessing to decompose complex logic
Processing asynchronous tasks with a coroutine: when you encounter some program requirements that need asynchronous processing, you can use a coroutine to implement them
Advantages of using CO process: simple and easy to implement
Example: using coprocessor instead of finite state machine (some modifications and comments are added based on the code of the original book)

//A class representing villagers
public class Villager : MonoBehaviour
{
    public float maxSatiation = 10f;        // Maximum satiety
    public float maxFatigue = 10f;            // Maximum sleepiness
    const float minSatiation = 0.2f;        // Minimum satiety
    const float minFatigue = 0.2f;            // Minimum sleepiness value
    private float satiation;                // Current satiety
    private float fatigue;                    // Current sleepiness value
    Coroutine currentCoroutine;                // Current status (collaborative process)

    //Onenable executes immediately when the script is activated
    void OnEnable()
    {
        satiation = maxSatiation;            // Initialize satiety and set it to the maximum value
        fatigue = maxFatigue;                // Initialize the sleepiness value and set it to the maximum value
        StartCoroutine(Tick());                // Start the process of "game cycle"
    }
    
    //Simulate the "game cycle", similar to the update method of monobehavior
    IEnumerator Tick()
    {
        //Cycle once per frame
        while(true)
        {
            DecrePerFrame(satiation);        // Reduce satiety
            DecrePerFrame(fatigue);            // Reduce sleepiness
    
            //If you are starving and the current state is empty, start the "eat" process and set "eat" as the current state
            if(satiation < minSatiation && currentCoroutine == null)
            {
                currentCoroutine = StartCoroutine(Eat());
            }
            
            //If you are sleepy, no matter what you are doing now, start the "sleep" process directly and set "sleep" as the current state
            if(fatigue < minFatigue)
            {
                currentCoroutine = StartCoroutine(Sleep());
            }
            
            //Pause one frame
            yield return null;
        }
    }
    
    IEnumerator Eat()
    {
        //Eat a little at each frame until you're full
        while(satiation < maxSatiation)
        {
            IncrePerFrame(satiation);
            yield return null;
        }
        
        //When you are full, the current status is changed back to empty
        currentCoroutine = null;
    }

    IEnumerator Sleep()
    {
        //Immediately stop what you are doing (e.g. "eating")
        StopCoroutine(currentCoroutine);
        
        //If you don't sleep enough, keep sleeping
        while(fatigue < maxFatigue)
        {
            IncrePerFrame(fatigue);
            yield return null;
        }
        
        //Enough sleep, the current status is changed to empty
        currentCoroutine = null;
    }
}

2.1.2. User defined interpolation formula
What is interpolation: define a new value / position between two values / positions
General interpolation formula: P01 = (1 – U) P0 + u P1;
What is u: in linear interpolation, u is a floating-point number between 0 and 1, which is used to determine whether the interpolation we obtain is closer to P0 or P1. (linear extrapolation u is < 0 or > 1, which is not commonly used in the game)
Common interpolation formulas
Linear interpolation: u = u
Jog: u = u * u
Slow out: u = 1 – (1 – U) * (1 – U)
Slow in and out: u = ((U – 1) (u – 1) (u – 1) + 1) ((u – 1) (u – 1) * (u – 1) + 1)
Sin wavelength: u = u + range (0, 1) sin(u 2 * PI)
2.1.3. Design of message module
Message / Event Management: there are often a large number of interconnected elements in the game, and they need the support of the message system very much.
Function of message / event: when an event occurs, the related results are triggered together (for example, if we kill an enemy, our achievement system needs to record that we kill one more enemy, which is the use of message / event)
Caching of message module: usually, after an event is triggered, all subscribed listeners will be notified immediately, but this does not apply to all situations (for example, when a player obtains a new weapon, the new weapon in the backpack will be highlighted, but the player has not opened the backpack at this time, but the event has already notified the listener when it is opened)
Example: simple implementation of message module (code based on the original book is simplified into pseudo code)

public class MessageManager
{
    Dictionary<string, Action<object[]>> messageDict;    // Stores a dictionary of messages and associated listeners
    Dictionary<string object[]> dispatchCacheDict;        // Buffer
    
    public void Subscribe(string messageKey, Action<object[]> action)
    {
        if(messageKey in messageDict.Keys)
        {
            //Set action as a new subscriber of messageDict[messageKey];
            //If the message already exists, add a subscriber (listener) to it
        }
        else
        {
            //Add new messageKey and new action to messageDict
            //Otherwise, add a new message
        }
    }
    
    public void Unsubscribe(string message)
    {
        messageDict.Remove(message);
    }

    public void Dispatch(string message, object[] args = null, bool addToCache = false)
    {
        if(addToCache)
        {
            //add message and args into cache
            //If you choose to join the cache, all incoming events are added to the cache
        }
        else
        {
            //trigger all the co-related actions in this message
            //Otherwise, all associated with the information are triggered
        }
    }

    public void ProcessDispatchCache(string message)
    {
        //If the message exists in the cache
        if(message in dispatchCacheDict.Keys)
        {
            //Executes all events associated with the message in the cache and then removes the message from the cache
            Dispatch(message, dispatchCacheDict[message]);
            dispatchCacheDicr.Remove(message);
        }
    }
}

2.1.4. Management and coordination between modules
Management of singleton mode
Prevent calling after destruction: add judgment in the single instance. If it has been destroyed, avoid calling
Prevent the single instance from being created repeatedly: because the single instance is generally marked as dontdestroyonload, the single instance will be created again during scene switching. Judgment needs to be added to avoid creating the single instance repeatedly
Script execution priority: in projectsetting, find the script execution order and set the script priority in it, which can effectively avoid nullreferenceexception (for example, if script a wants to obtain a single instance of script B in wake, but the priority of B is after a, then a cannot successfully obtain B, so it is necessary to set the priority of B higher than a)