2. A bit about me
I have been designing and programming video games for
over 30 years.
I am originally from the UK, live in the Netherlands and
now teach Video Game Programming at NHTV in Breda
(IGAD).
I am quite well known in the VG industry outside of the
US for making a series of games concerned with an
obscure sport known as Football, not to be confused with
the US popular sport "Handegg".
3. The focus of this talk
A = Autonomous
I = Interactive
Computers and their languages are generally good for A
and less good for I.
My focus is solving the problem at the programming
language level, rather than through the creation of
virtual machine architectures (such as Behaviour Trees).
4. Roman Numbers
The Romans had no symbol for 0.
The result was that it held back their development of
technology and science.
5. Doing Nothing
Do we lack an appropriate representation in computer
science for doing nothing?
6. Turing's Vision
Turing was a mathematician, and was interested in
computers as a device for solving problems.
Mathematically, there was no concept of doing nothing.
7. Data In, Data Out
IMMUTABLE COMPUTER IMMUTABLE
INPUT DATA ALGORITHM OUTPUT DATA
The program runs. The program terminates, all might be well.
The program runs. The program never terminates. Disaster.
If the program terminates before it has finished its job, that's an aberration.
Why on earth should one want to stop the program, apart from being too impatient to wait?
Doing nothing makes no sense; simply do not run the program.
8. It's a function, Jim
Computers were developed to process data in the manner of a function.
We would like these to execute in zero time.
Is the temporal nature of computation merely an inconvenience?
9. The interactive world is ongoing
The interactive world does not fit this model well.
Tasks take time and need to pass information between each other
asynchronously.
Computations must contend with mutable input data.
10. Mutable input data
We normally do not consider the possibility that the input data could change
while we perform computations on it...
What is the square root of 345 ?
OK working on it...
Nearly there...
Wait, sorry I meant 346...
!@@#$@!!
11. Mutable input data
This is unfortunately common in the real world, and a necessity in effective
game AI.
My GPS loves to say "Recalculating..."
12. The Polling Problem
Q: What do you get when you try to process unpredictable and time
dependent input with a Turing machine?
13. The Polling Problem
Q: What do you get when you try to process unpredictable and time
dependent input with a Turing machine?
A: A polling loop
While(!EscapePressed()) { // ugh, there must be a better way
if(pathPlanner->ExecSingleStep()) {
break;
}
}
14. Say "When"
While(true) {
if(!GlassAcceptablyFull()) {
// erm... do nothing???
yield();
} else {
Say("When!");
break;
}
}
15. Say "When"
While(true) {
if(!GlassAcceptablyFull()) {
// erm... do nothing???
yield();
} else {
Say("When!");
break;
}
}
21. If and When
If
● A change in state
● Dependent on current state
● When the If is executed
When
● A change in state
● Dependent on resulting state
● When a specified state change occurs
22. When?
Computers are not really designed for When. They are designed around
procedural logic, effectively a series of ifs executed in a sequence.
When is actually a more natural way to express responses to time
dependent input.
23. When you have Messages
The typical solution is to use messages, but this does not directly solve the
polling problem.
24. "When you get the message, do something else"
// pseudocode
function WaitForGlassFill() {
messageHandler = new Handler(OnGlassAcceptablyFull,
delegate(Message m) {
// code executed on message
// Exit from doing nothing
// But how?
});
SetAnimationState("ObserveFilling");
while(true) { // do nothing
Yield();
}
}
25. You have to keep checking for data changed by
message handlers
// pseudocode
function WaitForGlassFill() {
int onMessage=0;
bool stopFlag;
messageHandler = new Handler(eOnGlassAcceptablyFull,
delegate(Message m) {
stopFlag = true;
onMessage = eOnGlassAcceptablyFull;
});
SetAnimationState("ObserveFilling");
while(!stopFlag) { // do nothing
Yield();
}
switch(onMessage) { // ..etc..
26. You have to keep checking for data changed by
message handlers
// pseudocode
function WaitForGlassFill() {
int onMessage=0;
bool stopFlag;
messageHandler = new Handler(eOnGlassAcceptablyFull,
delegate(Message m) {
stopFlag = true;
onMessage = eOnGlassAcceptablyFull;
});
SetAnimationState("ObserveFilling"); Hello, Polling
while(!stopFlag) { // do nothing
Yield();
}
switch(onMessage) { // ..etc..
27. I wish I could simply say...
function WaitForGlassFill() {
SetAnimationState("ObserveFilling");
When(ReceivedMessage(eOnGlassAcceptablyFull)) {
SayWhen();
StartNormalAnimationState();
End; // this will stop this coroutine
}
DoNothing(); // until coroutine terminated
}
28. I wish I could simply say...
function WaitForGlassFill() {
SetAnimationState("ObserveFilling");
When(ReceivedMessage(eOnGlassAcceptablyFull)) {
SayWhen();
StartNormalAnimationState(); Here I have a trivial case,
but maybe I want some
End; // this will stop this coroutine
other tasks going on here
} too, like watching out for
a sniper.
DoNothing(); // until coroutine terminated
}
29. I want reusable behaviours
// anything can pause, it's a substate
// needs to be interruptible!
function Pause(int _count) {
int count=_count;
while(count-- > 0) {
yield;
}
end;
}
31. I want reusable behaviours
function WaitForGlassFill() {
SetAnimationState("ObserveFilling");
When(ReceivedMessage(eOnGlassAcceptablyFull)) {
SayWhen();
StartNormalAnimationState();
end;
} This needs to be
interuptable when
WaitForGlassFill handles a
while(true) { message and switches
Pause(Random(25,50)); task.
GlanceAtRandomPointOfInterest();
yield;
}
}
32. I want reusable behaviours
function WaitForGlassFill() {
SetAnimationState("ObserveFilling");
When(ReceivedMessage(eOnGlassAcceptablyFull)) {
SayWhen();
StartNormalAnimationState();
end;
}
And what if we want to do
something that is not
while(true) { instantaneous?
Pause(Random(25,50));
GlanceAtRandomPointOfInterest();
yield;
}
}
34. I want reusable behaviours
function WaitForGlassFill() {
SetAnimationState("ObserveFilling");
When(ReceivedMessage(eOnGlassAcceptablyFull)) {
SayWhen();
Pause(50);
StartNormalAnimationState();
end;
No! Can't do this. It would
}
cause the calling object to
Pause (at best).
while(true) {
Pause(Random(25,50));
GlanceAtRandomPointOfInterest();
yield;
}
}
35. Messages must change the "program counter"
function WaitForGlassFill() {
SetAnimationState("ObserveFilling");
When(ReceivedMessage(eOnGlassAcceptablyFull)) {
SwitchTo(GlassFullEnough);
}
State(Normal):
while(true) {
Pause(Random(25,50));
GlanceAtRandomPointOfInterest();
yield;
}
State(GlassFullEnough):
Pause(50); // delay before responding
// etc
}
36. Messages must change the "program counter"
function WaitForGlassFill() {
SetAnimationState("ObserveFilling");
When(ReceivedMessage(eOnGlassAcceptablyFull)) {
SwitchTo(GlassFullEnough); This concept is going
} against the grain of most
State(Normal): programming languages
and computer
while(true) {
architecture.
Pause(Random(25,50));
GlanceAtRandomPointOfInterest(); It has to be faked.
yield;
}
State(GlassFullEnough):
Pause(50); // delay before responding
// etc
}
37. Why Game AI is Difficult - Summary
● No direct language support for multitasking
● Where such support exists, there remains a lack of semantics for
switching execution state synchronously (that is, without polling)
● Polling does not scale well
● Complex time dependent interactions mean that without a proper
methodology behaviours must be kept simple to reduce complexity
38. Why Game AI is Difficult - Summary
Polling would seem to be at the heart of the problem.
Solutions should focus on this problem.
39. Why Game AI is Difficult - Summary
Typical solutions involve the creation of a virtual machine with a separate
domain specific language or encoding of programs in data structures rather
than code.
40. Virtual machine solutions
Script
Virtual Virtual
Real Domain
Machine Machine:
Machine Specific
(C#) Behaviour
(C++) Language
Trees
Multi-tasking
support
(yield)
41. Virtual machine solutions
Script
Virtual Virtual
Real Domain
Machine Machine:
Machine Specific
(C#) Behaviour
(C++) Language
Why can't we just
Trees
have a single
language as the
Multi-tasking solution?
support
(yield)
44. Messages: A light switch
// Oh nice this is now trivial...
// But only because there's no temporal behaviour
function LightSwitch() {
bool lit=false;
When(ReceivedMessage(eSwitchPressed)) {
lit != lit;
SetLightState(lit);
}
}
46. No Polling: A light switch
// pseudocode of language supporting coroutines, messages
// and state transitions
function LightSwitch() {
bool lit=false;
while(true) {
state UnlitState:
when(ReceivedMessage(eSwitchPressed)) { goto state LitState; }
lit=false; SetLightState(lit);
substate(DoNothing());
state LitState:
when(ReceivedMessage(eSwitchPressed)) { goto state UnlitState; }
lit=true; SetLightState(lit);
substate(DoNothing());
}
}
47. No Polling: A light switch with delay
// pseudocode of language supporting coroutines, messages
// and state transitions
function LightSwitch() {
bool lit=false;
while(true) {
state UnlitState:
when(ReceivedMessage(eSwitchPressed)) { goto state LitState; }
lit=false; SetLightState(lit);
substate(DoNothing());
state LitState:
when(ReceivedMessage(eSwitchPressed)) { goto state UnlitState; }
lit=true; SetLightState(lit);
substate(Pause(50)); // That was easy to add!
substate(DoNothing());
}
}
48. PROC system: A light switch with fade
function FadeTo(float to) {
do { float d = (to - light.currentBrightness)*0.1f;
light.currentBrightness += d;
if(d < 0.001f) {
end;
} } }
function LightSwitch() {
Timer startTime;
bool lit=false;
while(true) {
state UnlitState:
when(ReceivedMessage(eSwitchPressed)) { goto state LitState; }
substate(FadeTo(0));
substate(DoNothing());
state LitState:
when(ReceivedMessage(eSwitchPressed)) { goto state UnlitState; }
substate(FadeTo(1));
substate(DoNothing());
} }
49. The PROC system
● Started as coroutine system in 68000 assembler
● Later implemented in C and C++ where messages were added
● Most recently implemented in C# in Unity 3D
50. The PROC system
● Implements a HFSM with message handling protocols
● Implementation uses nested coroutines
● Has proven very effective at managing complex behaviours
● A solution within the programming language itself
51. PROC system architecture in C# and Unity 3D
IEnumerator method (a PROC)
Control object with a stop flag
looped switch statement
states Message handlers
Message handlers
states Message handlers
states Message handlers
states
Straight code
Substate iterators (polling for stop flag)
52. PROC system message handling
Under Attack
Base
Noise heard
Patrol
Goto next patrol point
Goto position
53. PROC system message handling
Under Attack
Base
Noise heard
Patrol
Message manager
Goto next patrol point
Noise heard
Goto position
54. PROC system message handling
Under Attack
Base
Noise heard
Patrol
Message manager
Goto next patrol point
Noise heard
Goto position
55. PROC system message handling
Under Attack
Base
Noise heard
Patrol
Message manager
Goto next patrol point
Noise heard
Goto position
56. PROC system message handling
Under Attack
Base
Noise heard
Patrol
Message manager
Goto next patrol point
Noise heard
Terminate
Goto position
57. PROC system message handling
Under Attack
Base
Noise heard
Patrol
Message manager
Goto next patrol point
Terminate Noise heard
58. PROC system message handling
Under Attack
Base
Noise heard
Patrol
Message manager
Investigate Noise
Noise heard
59. PROC system message handling
Under Attack
Base
Noise heard
Patrol
Message manager
Investigate Noise
Noise heard
…
60. Implementation in C# / Unity 3D
● Currently uses a pre-processor
● Message handlers are delegates inside methods that can access local
variables of the method
● Termination of a task is implemented using a flag which is polled for
● However, this polling is hidden
61. Conclusion
● At the heart of the difficulty of robust and rich behaviour in games is the
limitations of popular programming languages
● Many popular solutions create a virtual machine to avoid these problems,
often encoding the behaviours in data structures with domain specific
languages.
● However, with some small additions to existing languages, this problem
could be avoided
● Even without such languages, it is possible to create architectures that
solve the problem directly within the language (albeit inelegantly).
62. Conclusion
● The core problem is how to manage the response to input data that
changes over time
● This input data is not only from the player, but also between concurrent
tasks
● Polling is inescapable, but can be managed
63. Conclusion
● To solve these problems, which are connected not only with game AI, but
also with the general problems of scalable multi-tasking programs, you
can use this heuristic:
"Design your architecture to hide the
polling problem as much as possible"
64. Thank you
Dino Dini
email: dndn1011@gmail.com
Twitter: dndn1011