|
Portfolio von
Immanuel Scholz |
|
TL;DR;How do I get yield-like control flow in C++?Threads instead of scriptsThis article describes, how I use threads to model a natural C++ syntax and flow of logic for sequential, script-like actions. My terminology for "unit" and "strategy" is borrowed from my other article.Short: unit = agent. strategy = current state of an agent. Some strategies are basically sequences. "Do this, then do that, then do this." Typically for AI is a function "update" which updates the attributes of the unit. This function must not block but instead return "immediately". But some sequences of actions continue over a longer period of time. This is hard to model, if the function has to return immediately. Example: The strategy "confuse" is used when a unit is blocked by obstacles unexpectedly. "Confuse" first rotates the unit, then waits a couple of seconds, then rotates the unit again and finally wait again. (Actually, the "confuse" in Nightwatch is a bit more complex, but the simple version here will do for illustration). The ideal straight forward implementation would look like:
void update(float elapsed) {
unit.direction -= 0.3f;
sleep(1.5f);
unit.direction += 0.3f;
sleep(1.5f);
}
The implementation of "sleep(1.5f)" is what causes trouble here. Update must not
block! So you have to create different states for the phase before the each
sleep, for the sleep itself and after the sleep. Depending on this state, you
have to branch at each call to update.
The shortest (not necesarily the best readable) version I can come up with
looks like:
void update(float elapsed) {
switch (state_)
{
case 0: unit.direction -= 0.3f; state_++; sleeping_ = 1.5f;
case 1: if (sleeping_ > 0) {sleeping_-=elapsed; break;} else state_++;
case 2: unit.direction += 0.3f; state_++; sleeping_ = 1.5f;
case 3: if (sleeping_ > 0) {sleeping_-=elapsed; break;} else state_++;
}
}
This way of indirect flow logic is error prone, unnecessary complex and just
unnatural. Sure, it could be made a bit more readable but I doubt that it will
achieve the simplicity of my "ideal version" above.
A scripting language (cp. Python) is used for sequences of actions often. The
scripts have to be executable line-by-line (or rather statement-by-statement).
The current position within the script replaces the "state_" in my example above.
Execution is done until a blocking function like "sleep" is hit.
Scripting languages have a lot of different pros and cons. Sometimes they
can't be used. But even then, not all is lost. Normal threads can be used to
model agents with simple sequences of actions. I used this idea in my class
"Script". "Script" starts a new thread when created, which immediately blocks
until "update" releases it. While the thread is running, "update" waits until
the thread hits either a "sleep" or finishes execution.
DWORD WINAPI ScriptThread( void* param )
{
Script& script = *(Script*)param;
script.run();
script.scriptFinished_ = true;
SignalObjectAndWait(script.lock_, script.lock_, INFINITE, false);
return 0;
}
void Script::update(float elapsed)
{
if (!scriptThread_)
{
lock_ = CreateEvent(0, false, false, 0);
scriptThread_ = CreateThread(0, 0, ScriptThread, this, 0, 0);
assert(scriptThread_);
if (!scriptThread_)
{ // something wrong - break up script
CloseHandle(lock_);
finished_ = true;
return;
}
WaitForSingleObject(lock_, INFINITE);
}
if (sleeping_ > 0)
{
sleeping_ -= elapsed;
return;
}
if (sleeping_ <= 0)
{
sleeping_ = 0;
SignalObjectAndWait(lock_, lock_, INFINITE, false);
}
if (scriptFinished_)
{
SignalObjectAndWait(lock_, 0, 0, false);
CloseHandle(lock_);
finished_ = true;
}
}
void Script::sleep(float time)
{
sleeping_ = time;
SignalObjectAndWait(lock_, lock_, INFINITE, false);
}
Finally: The code for "Confuse" is identical with my "ideal example" above.
One final comment: Using this kind of multi-threading doesn't trigger any
typical multi-threading problems, as the two threads are never executed
simultanously.
PS: Of course, you would want to decrease the stack size of threads when
running many scripts in parallel! |
|