Interaction principle of C# synergy and Lua synergy

Posted by Mikell on Sun, 30 Jan 2022 18:48:11 +0100

Interaction principle of C# synergy and Lua synergy

In Unity game development, C# language + lua language is a common development mode. This paper mainly studies the basic principle of the interaction between C# coprocess and lua coprocess.

C #'s collaborative process

  1. C # coprocessor is essentially an iterator. (may be added later)
  2. yield return is a syntax sugar provided by C# that allows you to define iterators without explicitly.
  3. C #'s coroutine is non preemptive. It needs to actively call the MoveNext method of the iterator. In Unity, the engine is responsible for calling it.
  4. In Unity, the invocation of the coroutine is managed by the engine. When a coroutine is started, the corresponding iterator is added to the queue. At each frame, check whether the Current variable of the iterator meets the conditions (such as timing end and file download completion). If so, call MoveNext method and the coroutine will be executed downward.

lua's co process

  1. The covariance of lua is essentially a function.
  2. lua's cooperation process is also non preemptive. Call coroutine Resume() resume coroutine Yield() suspends the orchestration.
  3. lua's collaboration process management needs to be handled by itself. Generally, it is a main collaboration process + n sub collaboration processes. It is executed on the main collaboration process most of the time.

In lua, the timing function is realized in combination with C# co process

Upper code
main.lua

local yield_return = (require "util.cs_coroutine").yield_return

function Test1()
	local co = coroutine.wrap(function()
		for i = 1, 10 do
			yield_return(CS.UnityEngine.WaitForSeconds(1))  -- Timing 1 s
			print(i)
		end
	end)
	co()  -- Start collaboration
end

function Test2()
    local co = coroutine.create(function()
        print('coroutine start!')
        local s = os.time()
        yield_return(CS.UnityEngine.WaitForSeconds(3))  -- Timing 3 s
        print('wait interval:', os.time() - s)
 
        local www = CS.UnityEngine.WWW('http://www.u3d8.com')
        yield_return(www)  -- Wait for the file download to complete
        if not www.error then
            print(www.bytes)
        else
            print('error:', www.error)
        end
    end)
 
    coroutine.resume(co)
end

Test1()
Test2()

cs_coroutine.lua

local util = require 'util.luautil'
 
local gameobject = CS.UnityEngine.GameObject('Coroutine_Runner')
CS.UnityEngine.Object.DontDestroyOnLoad(gameobject)
local cs_coroutine_runner = gameobject:AddComponent(typeof(CS.Coroutine_Runner))
 
local function async_yield_return(to_yield, cb)  -- async_yield_return It's asynchronous
    cs_coroutine_runner:YieldAndCallback(to_yield, cb)
end
 
return {
	-- yield_return Is synchronized (looks synchronized)
	yield_return = util.async_to_sync(async_yield_return)
}

luautil.lua

--Pass in a async method
local function async_to_sync(async_func, callback_pos)
    --Return a sync method
    return function(...)
        local _co = coroutine.running() or error ('this function must be run in coroutine')
        local rets
        local waiting = false
        --Define callbacks on completion
        local function cb_func(...)
            if waiting then
                --Callback after waiting
                --Recover the collaboration from the suspended position and pass the results
                assert(coroutine.resume(_co, ...))
            else
                --If you call back directly without waiting, set the result directly
                rets = {...}
            end
        end
         
        --Parameter list add "callback" to the parameter list (add last by default)
        local params = {...}
        table.insert(params, callback_pos or (#params + 1), cb_func)
 
        --Call the asynchronous method and pass all the "parameters passed in through the synchronous method" and "callback" to the asynchronous method.
        async_func(unpack(params))
 
        --If it does not return immediately, it is marked as waiting and suspended
        if rets == nil then
            waiting = true
            --Hang! "coroutine.yield()" The return value of is coroutine.resume The second parameter passed in
            rets = {coroutine.yield()}
        end
        
        --Sync results back
        return unpack(rets)
    end
end

Coroutine_Runner.cs

using UnityEngine;
using System.Collections;
using System;
 
public class Coroutine_Runner : MonoBehaviour
{
    public void YieldAndCallback(object to_yield, Action callback)
    {
        StartCoroutine(CoBody(to_yield, callback));  //Start C# collaboration
    }
 
    private IEnumerator CoBody(object to_yield, Action callback)
    {
    	//to_yield is the yield in lua_ Return function parameters
    	//In the Test1() example, it is CS UnityEngine. WaitForSeconds(1)
        if (to_yield is IEnumerator)
            yield return StartCoroutine((IEnumerator)to_yield);
        else
            yield return to_yield;
        callback();  //The callback after the end of the association is the cb_ in the lua above. Func, restore lua synergy
    }
}

In general, the basic principle is:
After creating a lua coroutine, suspend the lua coroutine after the asynchronous operation starts, and resume the lua coroutine after the asynchronous operation completes and calls the callback method. Asynchronous operation is in C# cooperation. The mutation step is called to wait for synchronization.

The benefit is that the code looks synchronized. Of course, the disadvantages are also obvious. Every time yield is called_ The return function will create a C# coroutine; Creating a collaboration (whether lua or C#) will cause gc; Moreover, the created C# collaboration cannot be interrupted actively.

lua coprocess replaces C# coprocess

Calling code
main.lua

function Test()
    local start = (require "util.cs_coroutine").start
    start(function()
		print('coroutine start!')
        local s = os.time()
        coroutine.yield(CS.UnityEngine.WaitForSeconds(3))  -- Timing 3 s
        print('wait interval:', os.time() - s)
 
        local www = CS.UnityEngine.WWW('http://www.u3d8.com')
        coroutine.yield(www)  -- Wait for the file download to complete
        if not www.error then
            print(www.bytes)
        else
            print('error:', www.error)
        end
	end)
end

Test()

cs_couroutine.lua

local util = require 'util.luautil'

local gameobject = CS.UnityEngine.GameObject('Coroutine_Runner')
CS.UnityEngine.Object.DontDestroyOnLoad(gameobject)
local cs_coroutine_runner = gameobject:AddComponent(typeof(CS.Coroutine_Runner))

return {
    start = function(...)
        return cs_coroutine_runner:StartCoroutine(util.cs_generator(...))
    end;

    stop = function(coroutine)
        cs_coroutine_runner:StopCoroutine(coroutine)
    end
}

cs_coroutine_runner:StartCoroutine() needs to pass in an ienumeror parameter

public interface IEnumerator
{
	object Current {get;}
	bool MoveNext();
	void Reset();
}

Therefore, we should construct an IEnumerator class on the lua side
luautil.lua

local move_end = {}

local generator_mt = {
    __index = {
        MoveNext = function(self)
	        -- every time C#When calling MoveNext on the side, call coroutine on the lua side Resume() resume the process,
	        -- adopt coroutine.yield perhaps return return Current
            self.Current = self.co()  
            if self.Current == move_end then  -- lua The collaboration process has ended
                self.Current = nil
                return false
            else
                return true
            end
        end;
        Reset = function(self)
            self.co = coroutine.wrap(self.w_func)  -- establish lua Synergetic process
        end
    }
}

local function cs_generator(func, ...)
    local params = {...}
    local generator = setmetatable({
        w_func = function()
            func(unpack(params))
            return move_end  -- lua Return at the end of the collaboration move_end
        end
    }, generator_mt)
    generator:Reset()
    return generator
end

return {
	cs_generator = cs_generator
}

Compared with the above method, the C# cooperation process is constructed directly on the lua side. No matter how complex the cooperation process is (the number of coroutine.yield), there is only one lua cooperation process and one C# cooperation process, and the C# cooperation process can be interrupted actively.

summary

Unity's C# coroutine provides a way to handle asynchronous operations, which can make the code look more coherent and easy to understand. The same effect can be achieved on the Lua side through certain interaction means and in combination with the collaborative process of lua.

Topics: C# Unity Game Development lua