So today I played around with this.
First and foremost⌠you canât use time.sleep if youâre executing the script from the main thread because itâs on the main thread. Itâs the equivalent of sleeping the thread in C# with Thread.Sleep. So only use time.sleep if you know youâre on a different thread.
Next⌠python does have yield. Itâs called a generator function. I checked and ironpython does support it. Here is more on a generator function:
https://ironpython-test.readthedocs.io/en/latest/howto/functional.html
So what I did was create a scope that did a few things:
- gotoUnityThread - allow you to enter the main thread of unity if you arenât on it
- exitUnityThread - execute on a thread other than the unity thread
- coroutine - start up a coroutine (using the generator function from python)
- wait - get a WaitForSeconds object for use in a coroutine
I have 2 ways to start the script. On the main thread, and on async.
I also added in a way to reference the UnityEngine assembly⌠otherwise whatâs the point of being on the unity thread?
So the ScriptEngine from your github has changed a lot. First off I made it a MonoBehaviour so we can hook into the thing. Also, I moved the scope into its own class:
using UnityEngine;
using System.Collections.Generic;
using System.Threading;
using IronPython.Hosting;
/// <summary>
/// Python script engine wrapper which contain the Python scope used in an
/// application.
/// </summary>
public class ScriptEngine : MonoBehaviour
{
#region Fields
private Microsoft.Scripting.Hosting.ScriptEngine _engine;
private Microsoft.Scripting.Hosting.ScriptScope _mainScope;
private int _mainThreadId;
private System.Action<string> _logCallback;
private System.Action _joinMainThread;
private object _joinLock = new object();
#endregion
#region CONSTRUCTOR
private void Awake()
{
_mainThreadId = Thread.CurrentThread.ManagedThreadId;
}
public void Init(System.Action<string> logCallback)
{
_logCallback = logCallback;
_engine = Python.CreateEngine();
// Create the main scope
_mainScope = _engine.CreateScope();
// This expression is used when initializing the scope. Changing the
// standard output channel and referencing UnityEngine assembly.
string initExpression = @"
import sys
sys.stdout = unity
import clr
clr.AddReference(unityEngineAssembly)";
_mainScope.SetVariable("unity", new ScriptScope(this));
_mainScope.SetVariable("unityEngineAssembly", typeof(UnityEngine.Object).Assembly);
// Run initialization, also executes the main config file.
ExecuteScript(initExpression);
}
#endregion
#region Properties
public System.Action<string> LogCallback
{
get { return _logCallback; }
set { _logCallback = value; }
}
#endregion
#region Methods
public void ExecuteScript(string script)
{
this.ScriptStart(script);
}
public void ExecuteScriptAsync(string script)
{
ThreadPool.QueueUserWorkItem(this.ScriptStart, script);
}
#endregion
#region Private Methods For Engine
private void Update()
{
System.Action a;
lock(_joinLock)
{
a = _joinMainThread;
_joinMainThread = null;
}
if(a != null)
{
a();
}
}
private void ScriptStart(object token)
{
try
{
var script = _engine.CreateScriptSourceFromString(token as string);
script.Execute(_mainScope);
}
catch (System.Exception e)
{
Debug.LogException(e);
}
}
#endregion
#region Special Types
public class ScriptScope
{
public ScriptEngine engine;
public ScriptScope(ScriptEngine engine)
{
this.engine = engine;
}
public void write(string s)
{
if (engine._logCallback != null) engine._logCallback(s);
}
public void gotoUnityThread(System.Action callback)
{
if (callback == null) return;
if (Thread.CurrentThread.ManagedThreadId == engine._mainThreadId)
{
callback();
}
else
{
lock(engine._joinLock)
{
engine._joinMainThread += callback;
System.GC.Collect();
}
}
}
public void exitUnityThread(System.Action callback)
{
if (callback == null) return;
if (Thread.CurrentThread.ManagedThreadId != engine._mainThreadId)
{
callback();
}
else
{
ThreadPool.QueueUserWorkItem((o) =>
{
callback();
}, null);
}
}
public WaitForSeconds wait(float seconds)
{
return new WaitForSeconds(seconds);
}
public void coroutine(object f)
{
if (Thread.CurrentThread.ManagedThreadId != engine._mainThreadId)
{
this.gotoUnityThread(() =>
{
this.coroutine(f);
});
}
else
{
var e = f as System.Collections.IEnumerator;
if (e == null) return;
engine.StartCoroutine(e);
}
}
}
#endregion
}
Your ScriptRunner was truncated down. And I made it so that it can take a TextAsset so swapping out scripts is easy:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ScriptRunner : MonoBehaviour {
#region Fields
public TextAsset script;
private ScriptEngine _scriptEngine;
#endregion
#region CONSTRUCTOR
// Use this for initialization
void Start () {
_scriptEngine = this.gameObject.AddComponent<ScriptEngine>();
_scriptEngine.Init(Debug.Log);
}
#endregion
#region Methods
// Update is called once per frame
void Update () {
if (Input.GetKeyDown(KeyCode.Return))
{
_scriptEngine.ExecuteScript(this.script.text);
}
}
#endregion
}
And some example scripts.
First we show threading. The script defines to functions and then make sure itâs on the unity thread to run foo where it accesses stuff on the unity thread. Then it exits the main thread to then do your sleep loop:
import UnityEngine
from UnityEngine import Vector3
import time
def foo():
unity.engine.transform.position = Vector3(5,0,0)
unity.exitUnityThread(foo2)
def foo2():
for x in xrange(0, 5):
print 'TestPrint'
time.sleep(1)
unity.gotoUnityThread(foo)
This one demonstrates doing a coroutine in python. In it I start the coroutine where I wait a frame, print â1â, wait 5 seconds (using our custom wait method), prints 2, and then sets the position of the engine transform.
import UnityEngine
from UnityEngine import Vector3
def DoWork():
yield None
print "1"
yield unity.wait(5)
print "2"
unity.engine.transform.position = Vector3(5,0,0)
unity.coroutine(DoWork())
I created a pull request with my changes.