Embedding a language, and doing so productively, is one of my goals for Project Z. However, before writing a more technical post on how I’m using it, I wanted to explore just why using an embedded language seems, in theory, like a good idea. I certainly have been feeling a great deal of power, and I’m just starting on this path.
Put simply, embedding a language just means running code inside your code. You’ve got some code written in one language, and you’ve got some code written in another language. The first code runs the second code inside itself. This is not the same as the first program forking and calling another program. Some part of the first code is actually interpreting and actively running the second code. Pretty much any language can be embedded; you could embed Java into C, if you wanted to (don’t). In general, languages you’d want to embed share some properties. They’re dynamically typed. They’re lightweight. They are interpreted, or at least run under a VM.
This sounds pretty boring so far; why would you ever want to do this? These are sometimes nice properties for a language to have, but taken by themselves, they don’t really make you say “huh, that’s cool, I should use this language”. They are all means to an end, and that end is abstraction. Yes, that’s right, that old buzzword from your CS classes. It gets thrown around a lot, without too much explanation as to why it’s a good thing. To explain abstraction, let me step away from programming for a bit and offer this example.
Say you’re driving, and you want to turn left. Easy - just turn the steering wheel left. Everyone knows this; most people do it without even thinking about it. Even kids on their power wheels can figure this out. Now, try doing the same thing without the steering wheel. Hell, just try explaining the mechanical actions that take place when you turn your steering wheel left. Well, I dunno; something to do with the steering column that turns the tires in the right direction? Just give it a quick lookup on Wikipedia.
[From Wikipedia:] Many modern cars use rack and pinion steering mechanisms, where the steering wheel turns the pinion gear; the pinion moves the rack, which is a linear gear that meshes with the pinion, converting circular motion into linear motion along the transverse axis of the car (side to side motion). This motion applies steering torque to the swivel pin ball joints that replaced previously used kingpins of the stub axle of the steered wheels via tie rods and a short lever arm called the steering arm.
It turns out to be this complicated maneuver. And yet… you can do it with the simple turn of a wheel. You don’t need to worry about the details of transverse axes, of power steering, of racks and pinions, and so on. Sure, you might be able to figure it out after staring at the picture for a bit, but really, all you need to know is this: when you turn the steering wheel left, the car turns left. When you turn the steering wheel right, the car turns right. The more you turn the steering wheel, the more the car turns. This is the power of abstraction; hiding details away makes things easier to comprehend, and therefore easier to use, simply because you don’t need as much knowledge.
Bringing the discussion back to relevant topics: as game developers, we prefer not to have to worry about every single detail of how the game operates. We do not want to worry about small details - all we care about how entities within the game act and react. To draw an analogue to our steering wheel, consider player movement. All a game developer wants to do is this: when the key corresponding to “move left” is pressed, move the player to the left, and when the key corresponding to “move right” is pressed, move the player to the right. We don’t particularly care what the pointer address to the keyboard device is, or if the OS memory-maps the device, or if it supports DMA. Ideally, we shouldn’t even have to know that these events come from a keyboard. They could come from a joystick, or a mouse. All we want to know is when movement events are received, and we’ll take it from there.
In programming, the the steering wheel is called an interface. The interface is just a set of functions and (in OOP languages) objects, like Player.draw()
. Let the engine handle the details of creating an OpenGL quad and painting the sprite on it; we just want to display the character at a certain position, at a certain time. This is what the game engine does; it provides us a simple interface, as simple as a steering wheel, to be able to maniuplate the game world.
That’s the general overview of abstraction; the kind of abstraction an embedded language offers is actually a little different. You can and should provide such interfaces down to a scripting language, but you could just as well provide a C++ interface and just use C++ to write the game logic. The details of the engine are still hidden away. Why, then, would you opt to do all the extra work to bind your interface to another language? It’s all about the flexibility, in multiple senses, that scripting languages offer. To illustrate this, consider the following problem.
You’ve got a console in your game that you can call down using the tilde key like in Quake, or Morrowind (shown above). It takes text input & translates it to commands. For example, player->additem Gold_100 1000
gives the player 100 instances of the gold_100 item. These commands are basically just functions; in C++, to do the exact same thing, you’d probably just do player.addItem("Gold_100", 1000)
. The apparent solution is simply to map the additem
command to the player.addItem()
function. Okay, no problem - C/C++ has function pointers, so you can just use a std::map
or similar to map strings to function pointers.
But wait - function pointers are not the same as member function pointers. This is no problem for our simple example above - there’s only one player, and it’s probably either global or a singleton. However, if this is a multiplayer game, how do I know which player I’m giving gold to? Okay, well, then, I can use some function pointer to a manager class instead, like (World::*giveItemToPlayer) (int, string, int)
, and pass in the player ID as the first argument.
Forgetting about how ugly this syntax is, it works… until you want to call some stuff outside of World, like SpriteManager.ChangeSprite()
. Okay, well, no problem. World owns an instance of SpriteManager
- I’ll just put a function in World
, World::ChangeSprite()
, which forwards the command to the SpriteManager
. And I’ll do this for the NetManager
, and the CollisionManager
, and the MessageQueue
, and… suddenly your World
class becomes this huge bloated monster of a class. There are solutions to this, but none of them are particularly elegant.
I do know about `[std::bind](http://en.cppreference.com/w/cpp/utility/functional/bind)/[std::function](http://en.cppreference.com/w/cpp/utility/functional/function)` and their boost predecessors. They're somewhat elegant, but (1) using them introduces a huge dependency where you've got `std::function`s scattered around everywhere, and (2) I hesitate to actually discuss them because I'd have to go into more detail - the post is already pretty long.
So, how would we go about writing such a callback system in Squirrel? Well, all you need to do is store two things for each function; its environment (class) and the function name.
That’s it. In fact, here’s the (untested) code, in Squirrel, for arbitrary callbacks:
class CallbackManager {
callbackEnvMap = null;
callbackFuncMap = null;
constructor() {
callbackEnvMap = {};
callbackFuncMap = {};
}
function registerCallback(callbackName, env, funcName) {
callbackEnvMap[callbackName] <- env;
callbackFuncMap[callbackName] <- funcName;
}
function callFunc(callbackName, ...) {
local funcName = callbackFuncMap[callbackName];
callbackEnvMap[callbackName][funcName](vargv);
}
}
“Holy crap” was my first reaction to this. It’s mind-blowing after coming from C++’s strict typing (well, it’s not strictly strict typing, but it’s close). There are downsides to this - you don’t actually know what arguments the function takes, and you have to guess at the return type. Still, having to deal with these issues sure beats dealing with the baggage that comes with trying to do it in C++. This is just one of the flexibilities a language like this affords. Types as first-class members, automatic garbage collection, and nicer syntax are just a few more of the benefits you might get.
This flexibility is just another form of abstraction. All the embedded language’s features could be implemented in the host language (given Turing equivalence). However, again, we don’t care about all this; we can’t care about all of this. It’s just too much to think about. Once we have a scripting language, though, we have a much simpler, much more flexible language to work with. It makes tons of tasks easier as a developer; yes, it makes your game slower to some degree, but you can develop and maintain your code much more easily. That’s a tradeoff I am willing to make.