01 January, 2007

It's The Small Things...

Been working on the game engine all day today, and made a few minor adjustments that made a big difference. Since the Bitmap Font Generator can generate a font description file in XML, I decided to dump the parser I was using and switched to Microsoft's DOM parser for XML (built into Dolphin). This cut down on another 3rd party piece of code that may need to be maintained, and simplified the font code significantly.

What made it so much easier to code was Smalltalk's reflection capabilities, and being able to send dynamic messages to objects at runtime. I've used XML before for various programs, and many times, I end up with a piece of code that invariably looks like the following:
while(elt) {
if (!stricmp(elt->tagName, "info")) { ... }
if (!stricmp(elt->tagName, "common")) { ... }
if (!stricmp(elt->tagName, "pages")) { ... }

// next element
elt = elt->nextSibling;
}

And this is just terrible code. Terrible to maintain, terribly slow, and not scalable; as new data is added to the file, and new programmers need to add code to the above loop, it will just turn into one hell of a mess. I often see the inside of { ... } turn into more parsing code, instead of being broken out into another function. Eventually we end up with a several-hundred-line function that no one wants to touch for fear of breaking some unrelated piece of very fragile code.

So, how can the above be made easier (and more scalable) with Smalltalk and reflection? Well, each tag can just be turned into a method descriptor and called by the parsing code. The above in my font loading code looks like this:
[elt isNull] whileFalse:
[self perform: (elt tagName , ':') asSymbol with: elt.

"Next element."
elt := elt nextSibling]

So, the tag name itself is just used as the method that is called to parse it. In essence, the object just parses itself. Very slick. If there is ever a new tag added later on, I just add a new method to parse it and it should just work. Just the way code should.

Another nice, small, feature of the Smalltalk language is being able to cascade messages to the same object and a wonderful little method called #yourself. When possible, I like to code as functionally as possible (without sacrificing performance), and the above has allowed me to do this many times over. And - in my opinion - makes the code more elegant. Something one may see in C/C++ (or many other imperative languages) is the following:
bool SomeFunction()
{
if (SomeCondition) {
/* do something */
return true;
}
return false;
}

Now, there's nothing wrong with this. But it always feels a little dirty to me. You aren't really returning true or false from SomeFunction, instead, you are returning the result of SomeCondition, and that's not immediately apparent from reading the code. And, believe it or not, this does lead to bugs down the road when other programmers have to go in and do something to it.

So, how does message cascading and #yourself help in Smalltalk? Well, it allows me to perform actions based on the result of some statement or expression, and then actually return that result separate from the actions performed. The above could instead be coded:
^(someCondition)
ifTrue: [ "do something" ];
yourself

Anyone should be able to clearly see that someCondition is being returned, and other actions just happen to be performed based on that condition. That's going to be pretty hard to foul-up down the road. And, it looks nicer, too.

As I come across more Smalltalk elegance, I'll be sure to post them. But for today, these were the two that caught my eye the most often.

4 comments:

tejón said...

You should never have to write the same code twice.

This seems to be the guiding principle of good Smalltalking -- and most of the "wow!" moments I've had have centered on discovering this to be true in practice, not just in principle.

I'm mostly posting this comment to let you know that you do have a persistent audience on this blog. Smalltalk has been essentially ignored by game programmers (even Lisp has a better profile via Crash Bandicoot), and as a result there are no good tools available to fledgeling enthusiasts like myself. You appear to be rapidly patching that hole.

I really hope you make the final engine available to the public. Even if you decide not to, I'm seeing a strong enough proof of concept to motivate me to knuckle down and learn to do it myself if need be.

Keep it up!

Jason said...

+1 to tejon, and even if you don't want to make it available, I would be interested in hearing a price. I may never make anything, so I wouldn't be willing to spend thousands but we can talk. :)

Jason said...

Also, I didn't see this anywhere but what kind of game engine do you intend to make? Just a 2D one, or do you plan on going 3D at some point?

And if you do plan on going 3D, do you think green threads and Dolphin overlap calls will be enough?

I have been thinking of doing a smalltalk implementation (as much as I hate to think about so much non-smalltalk coding) to give pure multi-threading with transactional memory access (i.e. skip the fine grain locking hell of today).

Jeffrey Massung said...

First, thanks for the support! It is very good to know there are those out there that want this project completed as much as I do.

Right now I'm just doing 2D. Assuming it's as successful as I hope it will be, what I have should be easily extendable to 3D, and I plan on doing it. I don't want to dive right in to 3D first, only because there would be so much more to do (animation, shaders, shadows, physics, etc). But I would like to.

I actually haven't thought far enough ahead yet to wonder if green threads are going to be a problem moving into the realm of 3D. Given what I've been seeing so far, and where my typical bottlenecks end up being when I have them, I tend to think not.

Most of the heavy lifting in a 3D game is rendering and physics. Both of which will be done in 3rd party libraries (I have no desire to implement a physics package in Smalltalk).

I suppose once 2D gets far enough along, we'll find out. At least, I hope to find out. :-)