I'm writing a chess UI with Unity as well as an external engine (executable written in c#). I can send and receive data to and from the process (engine). This is working, But when the Make_Move method is called, when the process responds back with data, a problem occurs. While debugging the code execution just stops when trying to access the Unity objects in the Make_Move method, and The objects are missing namely the piece gameObject and sprite but the rest of the variables(that are not unity objects) is still there. I don't get any errors, all of the variables are part of a class that is kept in an array that keeps track of the piece it's sprite the gameObject as well as other things.
why would only the unity objects disappear from the object array?
why does code execution stop when trying to access the unity objects (sprite's etc)?
How to fix this?
Unity class for sending and receiving a 4 digit string, the start, and end position of the move to make (xyxy)
public static class UCI
{
static Process process = new Process();
static UCI()
{
ProcessStartInfo si = new ProcessStartInfo()
{
FileName = "ChessEngine.exe",
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardError = true,
RedirectStandardInput = true,
RedirectStandardOutput = true
};
process.StartInfo = si;
process.OutputDataReceived += new DataReceivedEventHandler(OnRecieved);
process.Start();
process.BeginErrorReadLine();
process.BeginOutputReadLine();
}
public static void SendLine(string command)
{
Debug.Log("send: "+ command);
process.StandardInput.WriteLine(command);
process.StandardInput.Flush();
}
public static void Kill()
{
process.Kill();
}
private static void OnRecieved(object sender, DataReceivedEventArgs e)
{
string text = e.Data;
Debug.Log("Recieved: " + text);
if(text.Length == 4)
Game.Make_Move(new Move(Board.Squares[(int)char.GetNumericValue(text[0]),(int)char.GetNumericValue(text[1])],Board.Squares[(int)char.GetNumericValue(text[2]), (int)char.GetNumericValue(text[3])]));
}
}
Unity make_Move method where the problem occurs:
static void Make_Move(Move move)
{
var end = move.end.GetPiece();
//posistion is a vector2Int, this line does not cause any trouble
print("Pos: " + Squares[move.start.GetPosition().x, move.start.GetPosition().y].GetPosition());
//this line causes execution to stop without error. if it is commented out execution will continue.
print("obj: " + Squares[move.start.GetPosition().x, move.start.GetPosition().y].gameObject);
//lines below this is not executed
SetPiece(move.start.gameObject.GetComponent<Image>().sprite, move.end.GetPosition(), end.MoveCnt);
RemovePiece(move.start.GetPosition());
// set the current turn to the other player
CurrentPlayer = CurrentPlayer == players[0] ? CurrentPlayer = players[1] : CurrentPlayer = players[0];
}
(Most of) The Unity API can not be used by background threads!
I don't know your types you are using/storing in that Squares
array but: be aware that most of the Unity API that directly influences or depends on the scene (e.g. any transform property, .gameObject
etc) is only available to the Unity main thread! (The exception is e.g. simple Vector3
operations as long as you don't assign them to a Transform
)
I'm pretty sure the issue here is that your OnReceived
is executed on a different thread (the one of the started process) => You don't even see the exception in the main thread!
A pattern often used in Unity is a so called "Main Thread Dispatcher" and might look like e.g.
public class MainThreadDispatcher : MonoBehaviour
{
// Here we store the current existing instance of this component
private static MainThreadDispatcher _instance;
// Public read-only access
public static MainThreadDispatcher Instance => _instance;
// A thread-safe Queue (first-in/first-out) for the actions
// that shall be executed in the next Update call
private readonly ConcurrentQueue<Action> _actions = new ConcurrentQueue<Action>();
private void Awake ()
{
// Make sure only one instance of this exists (Singleton Pattern)
if(_instance && _instance != this)
{
Destroy(gameObject);
return;
}
_instance = this;
// Optional but recommended: Make sure this object is not
// destroyed if the scene is changed
DontDestroyOnLoad (gameObject);
}
private void Update ()
{
// Run all actions that have been enqueued since the last frame
while(_actions.TryDequeue(out var action)
{
action?.Invoke();
}
}
// Call this from any thread to make an action be executed in the next Update call
public void DoInMainThread(Action action)
{
// Simply add the action to the end of the thread-safe Queue
_actions.Enqueue(action);
}
}
Then you would in your class do
private static void OnRecieved(object sender, DataReceivedEventArgs e)
{
var text = e.Data;
Debug.Log("Recieved: " + text);
if(text.Length == 4)
{
MainThreadDispatcher.Instance.DoInMainThread(() =>
{
// Delay this code line until we are in the next unity main thread
// "Update" call where Unity API can be safely used
Game.MakeMove(new Move(Board.Squares[(int)char.GetNumericValue(text[0]),(int)char.GetNumericValue(text[1])],Board.Squares[(int)char.GetNumericValue(text[2]), (int)char.GetNumericValue(text[3])]));
});
}
}
Now all you have to do is make sure you have the MainThreadDispatcher
component attached to any always active GameObject in your first scene.