Is there a way to pause and continue chunk execution #133
-
Hello, I hope this is the right place to ask this question. I am trying to create a game inspired by Colobot in which the player can control entities by writing scripts in Lua. Below is a simplified version of the class that executes a script for a specific entity in the game. The script can make entities perform certain actions by using predefined functions like "move()" that sets a property on the entity object which will ensure that the action is then performed during the game loop. class ScriptManager()
{
private static readonly Lua Lua = new();
private dynamic Environment;
Luachunk Script { get; set; }
GameEntity Entity { get; }
private readonly Action<int> Move;
private readonly Func<Vector2> GetPosition;
public ScriptManager(GameEntity entity)
{
Entity = entity;
Move = (int distance) => MoveEntity(Entity, distance);
GetPosition = () => GetEntityPosition(Entity);
}
private void CreateLuaEnvironment()
{
Environment = Lua.CreateEnvironment<LuaGlobal>();
Environment.move = new Action<int>(Move);
Environment.getPosition = new Func<Vector2>(GetPosition);
}
public void Init(string script)
{
try
{
Script = Lua.CompileChunk(script, "script.lua", new LuaCompileOptions() { DebugEngine = LuaExceptionDebugger.Default });
}
catch (LuaParseException e)
{
ShowError($"Exception on line {e.Line}: {e.Message}");
}
}
public void Start()
{
CreateLuaEnvironment()
Execute(Script);
}
private LuaResult Execute(LuaChunk luaChunk)
{
try
{
return Environment.dochunk(luaChunk);
}
catch (LuaRuntimeException e)
{
ShowError($"Exception at '{e.Line}': {e.Message}");
}
return null;
}
private static void MoveEntity(GameEntity entity, int distance)
{
// the statement below is actually added to a queue of type Action
entity.MoveDistance = distance
}
private static Vector2 GetEntityPosition(GameEntity entity)
{
return entity.Position;
}
} To keep the example simple I omitted some of the code. An important part that I did not include is that the action under For example print(getPosition()) -- shows (0, 0)
move(1)
print(getPosition()) -- still shows (0, 0) as the action has not yet been performed Ideally I could somehow stop the chunk execution on an action and then continue where it stopped after the entity has performed that action. But I can`t figure out a way to do this and I fear that this is not possible. Does someone know of a way I could implement something like this or are there perhaps better approaches to achieve this? |
Beta Was this translation helpful? Give feedback.
Replies: 2 comments 3 replies
-
Hui, there is no easy answer for your question. Basic rule: A lua chunk is compiled to machine code an will be executed on the calling thread. To spawn and stop execution of an thread you need in minimum two threads (But you can also take use of the .net thread pooling or async/await pattern). So, you need a control thread and at least one script thread, that/they works/synchronize together. The good part, NeoLua is pure .net. All knowledge of threading is your tool chain. The bad news, it is a lot of new stuff to learn. Greatings. |
Beta Was this translation helpful? Give feedback.
-
While the above solution works fine in .NET Framework, it doesn't work in .NET Core since the Thread.Abort function has been essentially removed. (Function still exists, it does nothing ). Therefore, we have to use CancellationToken instead. I thought I would provide my solution which @neolithos has hinted at being possible. It took me about a day to figure out how to do it, so I thought it might help as at least a starting place the solution I arrived at. This solution still requires two threads. However that is not shown in the code below, since how the threads are set up is application specific. What I am showing here is how to have the compiled lua code respond to a cancellation token and abort. There is a cost to this at runtime since we check the token quite often. However I don't currently see a way around this as it MUST be checked inside every loop or there could be the possibility of a runaway loop. Pausing could be done in a similar manner - using a ManualResetEvent instead of (or in addition to) the cancellation token. //An aborable ILuaDebug class that uses cancellation tokens to abort
public class AbortableLuaDebug : ILuaDebug
{
CancellationTokenSource m_tokenSource;
public LuaDebugLevel Level => LuaDebugLevel.None;
public AbortableLuaDebug(CancellationTokenSource tokenSource)
{
m_tokenSource = tokenSource;
}
public class AbortableExpressionVisitor : ExpressionVisitor
{
public AbortableLuaDebug m_debug;
public AbortableExpressionVisitor(AbortableLuaDebug luaDebug)
{
m_debug = luaDebug;
}
//Here is where all the work is done.
//At the start of every block, and after each label in the block we check the cancellation token
//and throw an exception if the result has been cancelled.
protected override Expression VisitBlock(BlockExpression node)
{
Expression expCheckCancel = Expression.Call(Expression.Constant(m_debug), new Action(m_debug.CheckAbort).Method);
List<Expression> children = new List<Expression>(node.Expressions.Count+1);
children.Add(expCheckCancel);
//Add the node at the top of the block, and then any time under a label
for(int iExp=0; iExp<node.Expressions.Count; iExp++)
{
Expression expChild = node.Expressions[iExp];
children.Add(expChild);
//Cannot be after a label that is the last expression
if(iExp <node.Expressions.Count-1 && expChild is LabelExpression)
{
children.Add(expCheckCancel);
}
}
return base.VisitBlock(node.Update(node.Variables, children));
}
}
public void CheckAbort()
{
m_tokenSource.Token.ThrowIfCancellationRequested();
}
//Abort the script execution
public void Abort()
{
m_tokenSource.Cancel();
}
//This class is only necessary because Chunk only has no public constructor
private class AbortableLuaChunk : LuaChunk
{
public AbortableLuaChunk(Lua lua, string name, Delegate chunk) : base(lua, name, chunk)
{ }
}
public LuaChunk CreateChunk(Lua lua, LambdaExpression expr)
{
LambdaExpression expAbortable = new AbortableExpressionVisitor(this).Visit(expr) as LambdaExpression;
return new AbortableLuaChunk(lua, expr.Name, expAbortable.Compile());
}
}
class LuaScriptRunner
{
//An example of how one might use this - this bit needs to be adapted to your environment
public static LuaResult DoString(CancellationTokenSource tokenSource, Lua engine, LuaGlobal environment, string script)
{
AbortableLuaDebug luaDebug = new AbortableLuaDebug(tokenSource);
LuaCompileOptions compileOptions = new LuaCompileOptions();
compileOptions.DebugEngine = luaDebug;
LuaChunk chunk = engine.CompileChunk(script, "script", compileOptions);
return environment.DoChunk(chunk);
}
} |
Beta Was this translation helpful? Give feedback.
Hui,
there is no easy answer for your question.
Basic rule: A lua chunk is compiled to machine code an will be executed on the calling thread.
To spawn and stop execution of an thread you need in minimum two threads (But you can also take use of the .net thread pooling or async/await pattern).
So, you need a control thread and at least one script thread, that/they works/synchronize together. The good part, NeoLua is pure .net. All knowledge of threading is your tool chain. The bad news, it is a lot of new stuff to learn.
Greatings.