Notes and extensions of Unity3D action game development practice 2.1

2.1.1. Using coprocessing to decompose complex logic

  • Processing asynchronous tasks with a collaborative process: when you encounter some program requirements that need asynchronous processing, you can use a collaborative process to implement them
  • Advantages of using collaborative 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 starve to death and the current state is empty, start the "eating" process and set "eating" 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 currently 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. Custom 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 extrapolated u is < 0 or > 1, which is not commonly used in games)
  • Commonly used interpolation formula
    • Linear interpolation: u = u
    • Jog: u = u * u
    • Slow out: u = 1 - (1 - u) * (1 - u)
    • Slow entry and exit: 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 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 another 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 the 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;	//A dictionary that stores 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 will be added to the cache
		}
		else
		{
			//trigger all the co-related actions in this message
			//Otherwise, all associated with this 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 the repeated creation of single instances
  • 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 get A single instance of script B in wake, but the priority of B is after A, then A cannot successfully get B, so it is necessary to set the priority of B higher than A)

Reference: development practice of Unity3D action game - Zhou Shangxuan

Keywords: C# Unity Design Pattern Game Development Unity3d

Added by zysac on Mon, 07 Feb 2022 13:11:01 +0200