We've also been there done that (about 10 years ago though), we had a very powerful scripting approach integrated into our game engine which gave direct access to game play systems in order to let our level and game designers build scripted behaviour into the game. In the end we ended up with a terribly huge mess of script code (I think it was about a third of the actual C/C++ code) and the majority of the per-frame performance-budget was lost somewhere in this scripted mess. The game sometimes suddencly crawled to a halt when some crazy scripting construct was called, and we had a lot of trouble getting stuff into a shippable state until the gold-master milestone (this is the game: http://www.metacritic.com/game/pc/project-nomads).
The main problem with scripting layers is that you are basically handing programming tasks over to team members who's job is not to solve programming tasks, and thus getting a lot of beginner's code quality and performance problems which are almost impossible to debug and profile (unless you have a few top-notch coders in the game- and level-design teams).
And then there will be definitely those "creative workarounds" to get something done which neither the engine nor the scripting layer was designed for, which make entertaining horror stories the programmers tell the new hires when they inevitable ask why your engine doesn't have scripting ;)
A better approach is to give the level designers simple, pre-programmed, combinable high-level building blocks (AI behaviours, actions, triggers, etc), and let them customize a level (as in game area) with this. But never build the entire game logic with such an approach! With this, stuff can still be fucked up, but at least the performance-sensitive stuff can be implemented by the programming team, and it's much easier to debug and maintain.
The slightly obscure music programming language SuperCollider [1] added co-routines about 10 years ago and they became one of my beloved techniques. Was very glad to see them come to python and soon to mainstream javascript.
Boost has a c++ implementation but it looks quite different:
> Was very glad to see them come to python and soon to mainstream javascript.
I really need to get around to writing a blog post to explain this in detail since this misapprehension is endemic. Python and JavaScript do not have coroutines, they have generators. Lua has actual coroutines.
The latter is dramatically more expressive than what you can do with what Python, JavaScript, and C# offer. This mistake drives me crazy because it means people don't know what they're missing.
Here's a quick example. Let's say we've got a little Python class for binary
trees:
class Tree:
def __init__(self, left, data, right):
self.left = left
self.data = data
self.right = right
We'll add a method to do an in-order traversal. It takes a callback and invokes the callback for every data value in the tree, like so:
def walk(self, callback):
"""Traverse the tree in order, invoking `callback` on each node."""
if self.left:
self.left.walk(callback)
callback(self.data)
if self.right:
self.right.walk(callback)
We can create a little tree and then print the data items in order like so:
(This works in Python 3, in Python 2, you'll have to make a little fn for print.) Swell, right?
Later, we decide we want to iterate over the items in a tree. Easy-peasy, Python has generators! We can just make a function that takes a Tree and returns a generator. We already have a method to walk the nodes, so we just need to call that and then yield the items, like so:
def iterateTree(tree):
def callback(data):
yield data
tree.walk(callback)
Then you can just use it like so:
for x in iterateTree(tree):
print(x)
Perfect, right?
Actually, no. This doesn't work at all. You can't yield from the callback passed to walk. That's because walk() itself doesn't know that the callback is a generator.
This is the problem with generators: they divide all functions into two categories: regular functions and generators. You run a regular function by calling it. You run a generator by iterating over it. The caller must use it in the correct way.
At its simplest level it means you have to be careful when refactoring. If you have a generator function that gets too big and you want to split it up, you have to remember that the functions you split out are also special generator functions if they contain a yield. You have to remember to flatten it when you "invoke it".
It's more than just annoying though: it means it's impossible to write code that works generically with both kinds of functions. In other words, all of your higher-order functions like map, filter, etc. now only work with some of your functions. (Or, I suppose, you could explicitly implement them to support both but that's more work and I don't think most languages do.)
In languages like Lua, the above code just works. You can yield from anywhere in the callstack and the entire stack is suspended. It's fantastic.
(If I can be forgiven a bit of self-promotion, I'll note that my programming language Wren[1] can not only express full coroutines like Lua, but also supports symmetric coroutines which can express some things Lua cannot. They are roughly like the equivalent of tail call elimination for coroutines.)
and the pattern library is built entirely around embedding in streams and yielding others streams. it uses this for very interesting numeric music patterns.
result = yield from future – suspends the coroutine until the future is done, then returns the future’s result, or raises an exception, which will be propagated. (If the future is cancelled, it will raise a CancelledError exception.) Note that tasks are futures, and everything said about futures also applies to tasks.
result = yield from coroutine – wait for another coroutine to produce a result (or raise an exception, which will be propagated). The coroutine expression must be a call to another coroutine.
Yield and generators (i.e. save stack; return value to caller; receive value from caller) are really a language feature for writing a runtime for a different language with coroutines. Or if you're willing to write your program in a way that looks like it was generated by a source-to-source transformation tool, you can write your coroutines on top of them. The most basic construct is something like this:
CurrentCoroutine = None
def run(main, arg):
global CurrentCoroutine
CurrentCoroutine = main
while CurrentCoroutine is not None
CurrentCoroutine, arg = CurrentCoroutine.send(arg)
def corodecorator(coro):
@functools.wraps(coro):
def init():
c = coro()
c.next()
return c
return init
And this is pretty much it. A simple example for two coroutines that pass control to each other would be:
As far as I can tell, that's still just a little local syntactic sugar for making composing generators nicer. There's a still a fundamental distinction between generators and other functions.
Think of it like this. Let's say you have a chunk of code that you want to refactor out into a separate method. The usual way to do that is to pull it out into a separate method and then call it from the place where the code used to be inline.
In languages with coroutines, you can just do that, regardless of what's in that chunk of code. In Python, you have to think, "Oh, does this chunk of code contain a yield?" If so, you need to do a "yield from" the function you pulled out instead of a regular call.
It forces you to constantly be cognizant of and design around the split between normal code and generators.
I've been using threads to do co-operative multi-tasking, for a while now.
Every place that I'm tempted to write an event-driven finite state machine, or something similar, I spawn a thread instead. I get to write synchronous code, which feels much more natural to me.
For instance my actor, running in a thread, calls a function like advance(). That drops data into an object, and wakes up the main thread, and blocks.
The next time the main thread wants to give processing time to the actor, it describes the world into the same shared object, waked up the actor's thread, and blocks.
Doing a switch like this dozens or even hundreds of times per second seems to work pretty well, especially if the main thread only gives execution to the actor thread when it needs to - inputs have changed, etc.
For my use cases, it radically simplifies my code, and I have a small number of different inputs to handle, so it has been scaling well.
That is precisely the beauty of coroutines: your code can look like threaded code but you can get the performance of an event-loop. Put another way, with coroutines you get most of the advantages of an event loop but very few of its disadvantages. (You get concurrency only, not parallelism, well this is almost true). For cooperative multi-tasking, threads are unnecessarily resource hungry and wasteful. They hoard resources although most of the time they are doing nothing just waiting. This is usually fine if you think yours is the only application that should be running on the hardware at that time, but typically you want to share the hardware with others.
Remember not everybody has the luxury of working on a system where you can spawn 10,000 threads without breaking a sweat.
However this territory of literally hundreds of thousands of programmed agents participating in a game does not seem to be very populated. Perhaps part of the reason is that very few languages had efficient (this eliminates stack copying), scalable and portable support of coroutines. This is starting to change, but not as fast as I would like.
I think C is to be blamed for the long under appreciated status of coroutines. It is one abstraction that C left out, although the VM C had as its execution model (the PDP) had excellent support for coroutines at the instruction level. C exported pretty much every abstraction of the underlying instruction set, but not coroutines.
EDIT: @VikingCoder Replying here as HN wouldnt allow me to respond till some time has past. Yes I have looked at asio although just scratched the surface. It looks very interesting, as far as I know they are not threads though (which is a good thing), they use macro and template metaprogramming trickery to turn producer-consumers into one big switch case. If you interested in coroutines and seamless interaction with C++ I can recommend http://felix-lang.org
> C exported pretty much every abstraction of the underlying instruction set, but not coroutines.
My hunch is that the designers of C would have said "goto" and "switch" cover the use case where you have a bunch of peer chunks of code that you want to freely bounce between.
Remember, at the time function calls were considered expensive, so not support full coroutines across function call boundaries may not have been on their minds as much.
Indeed, and thanks for the excellent commentary on coroutines, awaiting the blog post. I think another fact played into their decision: coroutines do not specify how they are to be scheduled, that leaves room for arbitrary policies. Not that C shied away from keeping things undefined.
By the time I got within touching distance of any computer the era of the PDPs were long over. I have learned from John Skaller that they could exchange control between two stack frames in a single assembly instruction. "Exchange Jump" is what I think it was called. You will have to search the assembly manual for PDP-11 for more. Wikipedia has some details http://en.wikipedia.org/wiki/Coroutine#Implementations_in_as... But I am sure there are HN readers who can speak with way more authority and exhaustiveness than the wikipedia page and can probably point find you a PDP-11 manual. I think you will find this thread interesting http://permalink.gmane.org/gmane.org.user-groups.linux.tolug...
Quoting the most interesting bits from that thread, (although I urge you to read the original):
Of the many styles of subroutine calls on the PDP-10, JSP ac,addr is the fastest,
as it's the only one that doesn't require a memory store.
Its ISP is something like:
ac = PC
PC = effective address [addr in the usual case]
The subroutine return, of course, is:
JRST (ac)
Here, the efective address is the contents of the register.
The coroutine instruction combined the two:
JSP ac,(ac)
This essentially exchanged the PC with ac.
Hi srean - thanks for that recap.
I just did some more digging on this and tried to understand the assembly versions of coroutines. They were very spartan. It was just: POP the next address from the stack into TEMP, PUSH the current PC, then set the PC to TEMP. Notice that there isn't any linking or parameter passing.
Overall, it has been fun reading on all the variants of this idea.
side note: in my first job, there were a few PDP-11s in the lab that I was responsible for. We never turned them on though.
Also, the PDP 10, which you mention above, was one of the most revered machines by hackers.
Don't you still need to carefully use locks everywhere? For me the one reason to use coroutines instead of regular threads is that coroutines are cooperative multitasking, rather than preemptive. Which is what I want for when I have a bunch of concurrent but not parallel processes working on shared data.
I have a main thread, an actor thread, and a single object that they use to communicate. So yes, I have one lock, around the single object they use to communicate.
I'd need one lock per actor thread and its communication object.
I say again, this works in my problem domain, and probably wouldn't work in other domains.
UE4 has hot reloading of C++ code. Some of their demos were pretty impressive. However I'd have to use it for a long period of time on a "real" project to see how reliable it truly is.
By removing the power from your design team you are creating more work for the software engineers and removing tools from the creative team.
An ideal scripting language:
- Allows designers to build complex gameplay elements, define complex ai behaviors, create gameflow with minimal work from the software engineers.
- Handles memory allocation/destruction behind the scenes
- Does not crash the game when an error occurs
- Handles multithreading and/or events
- Does not allow designers to shoot themselves in the foot
- Has a simple and clean syntax
- Allows software engineers to expose their APIs to it easily
- Can be reloaded on the fly
If you control the feature set of the scripting language then it becomes a crucial tool for rapid development of your game - empowering the creative team to make their game and allowing the software engineers to concentrate on all the other stuff that needs their attention.
In my opinion visual scripting and component systems can be a useful addition to an engine, but I have always seen a usefulness in having a scripting language layer.
(I have shipped multiple high profile console games with more lines of script code than of game code and design teams matching the size of the programming teams)
Did you use an existing scripting language, or roll your own? How did you handle debugging and profiling, and did you limit access to control constructs like loops, variables, etc..? Did the scripting system have some sort of per-frame budget/quota? Curiously interested...
Rolled our own - one I helped write, one I used. Both followed a stripped down C-like syntax (minus pointers). Both had conditional statements, loops, variables, simple structs, arrays (range checked), float/int/bool.
On the projects I worked on script performance wasn't too much of an issue, the scripts were used to control state and flow, not to do any number-crunching.
If you control what is exposed to the scripting then engineering will know when design asks for access to something that should be implemented outside of scripts. Which in my experience seemed to have minimized performance issues.
One concerns when switching over to a development system such as Unity or Unreal 4 is that your gameplay 'scripts' have access to the entire engine. It seems very easy at that point for your game to turn to unintentional spaghetti.
> The main problem with scripting layers is that you are basically handing programming tasks over to team members who's job is not to solve programming tasks, and thus getting a lot of beginner's code quality and performance problems which are almost impossible to debug and profile
I disagree that it has to follow that scripting -> bad code. I think of the C <--> scripting integration that I do as a Judo secret weapon. I get to easily fling "high-performance" C around in a super-dynamic way (REPL, easy-to-use high-level abstractions)... I think the problem you're describing is real (possibility to abuse/mis-use scripting power), but dismissing the notion of scripting plus C/C++ because of possibility of abuse seems like throwing out the baby with the bathwater. Better addressed with training and culture.
Yes I actually agree, it's more of an organizational problem of who does what in a team. For fast prototyping, or working in a small team of experts it will be an advantage. There are only very few people which are gifted to be both a great artist and a pragmatic programmer who cares about code quality and performance. I think the right middle ground is that the programming team provides recombinable building blocks, which are low-level enough that combining them makes sense, but high-level enough that they are still easily controllable and maintainable. The UE4 blueprints sound like this.
> The main problem with scripting layers is that you are basically handing programming tasks over to team members who's job is not to solve programming tasks, and thus getting a lot of beginner's code quality and performance problems which are almost impossible to debug and profile (unless you have a few top-notch coders in the game- and level-design teams).
It sounds like having a real software engineer do code reviews and/or rewrite the poor scripts before they get committed would be a solution to this problem. Presumably giving level designers the ability to script things is good for the game play.
The main problem with scripting layers is that you are basically handing programming tasks over to team members who's job is not to solve programming tasks, and thus getting a lot of beginner's code quality and performance problems which are almost impossible to debug and profile (unless you have a few top-notch coders in the game- and level-design teams).
And then there will be definitely those "creative workarounds" to get something done which neither the engine nor the scripting layer was designed for, which make entertaining horror stories the programmers tell the new hires when they inevitable ask why your engine doesn't have scripting ;)
A better approach is to give the level designers simple, pre-programmed, combinable high-level building blocks (AI behaviours, actions, triggers, etc), and let them customize a level (as in game area) with this. But never build the entire game logic with such an approach! With this, stuff can still be fucked up, but at least the performance-sensitive stuff can be implemented by the programming team, and it's much easier to debug and maintain.
[edit: typos]