07 March, 2007

Extending and Simplifying

Over the past week I've been slowly putting together a rather long post about all the changes that have been made. Instead of trying to make points or talk about experiences, I think this time around I'll just give the updates and explain a couple decisions made along the way.

I'll start with the biggest update: the game engine is now using OpenGL. I still have all my Direct3D9 wrappers (and anyone is welcome to them), but OpenGL offered [for this project] some extremely nice benefits over Direct3D. I'll get to those in a minute. But, for now, know that Dolphin now has a 100% implementation of OpenGL out there, with extension support.

I've also decided to implement 2/3's of an MVP triad (I'm still trying to wrap my head around MVP): the view and the presenter. This not only simplified my code a ton, but I imagine than an OpenGLPresenter would be extremely useful for others in the Dolphin community as well. Probably the smallest example I can give of it in action would be:
p := OpenGLPresenter show.

p makeCurrent
ifTrue: [(OpenGLLibrary default)
glClear: GL_COLOR_BUFFER_BIT].
p flip.
Simple enough. There should now be a rather large OpenGL rendering window on the desktop with nothing in it. The presenter can be attached to any view object (within reason I suppose), which means it should be very easy to use in dialog applications or anywhere else that rendering is needed.

Since the ExternalLibrary object uses its own #getProcAddress: for looking up external functions, this made creating OpenGL extensions very easy. Subclassed off of OpenGLLibrary is OpenGLExtension, which overrides the #getProcAddress: method and replaces it with wglGetProcAddress(). Also, it has a class method: #find. To use an OpenGL extension, just subclass the extension class, and then just start defining the instance methods as normal, just as if the extension exists. The #find method will search the extensions available against the class name and return it if found, otherwise nil.
"Declaring the extension class..."
OpenGLExtension subclass: #ARB_multitexture

"Defining one of the methods in the extension..."
glMultiTexCoord2fvARB: mode coords: v
<stdcall: void glMultiTexCoord2fvARB dword float*>
^self invalidCall

"Using the extension..."
ARB_multitexture ifFound: [:ext |
ext glMultiTexCoord2fvARB: GL_TEXTURE0_ARB coords: ptr].
That's it! Extensions in OpenGL have never been easier (in my experience).

Okay, so why the switch? Well, there were a few different reasons. The primary one is that all games are different. Sure, I'm making a 2D engine, but there are many flavors of 2D games. Each with its own set of requirements and rendering needs. And there's no way I could (or would want to) anticipate all of them.

With Direct3D, there were two big problems. First, D3D is handled entirely through a single interface object (the IDirect3DDevice9). This means that if I (or someone else) down the road wanted to do any special sort of rendering, it would require that I make the device available to everyone. And that brings me to the second problem: D3D state management can be a nightmare. Simply put, I couldn't trust external code to properly manage the device and the state. If the device were to get in a mucked state, it could cause serious problems (including image corruption).

Using OpenGL, not only is rendering easier and well understood by most hobby game developers, state is managed through stacks. Allowing outside code to push transforms, enable client states, render, then pop the state back to what it was is trivial. The engine won't be making use of client states or vertex arrays - just display lists and primitive OpenGL functionality. This means future games aren't limited by what I do now.

Asteroids does some of its own rendering (stars and thrust particles).

This allowed me to completely rid the engine of the GameRenderer class and a whole host of code that was just a waste of space. It always feels good when extending functionality actually reduces the overall code and what needs to be maintained.

Another benefit that I'll take a minute to mention was shown above in the OpenGLPresenter example. That is, while the game is running I can send OpenGL commands to the game to test ideas from a workspace. This is a wonderful debugging tool to have at my disposal, and I use it constantly to test state, get error codes, or even try out different rendering techniques.

Okay, so what else? It's been a while since my last progress update.

I'm still using DirectInput, and I have joysticks and gamepad controllers working. I've ported over the XINPUT libraries (for Xbox 360 controllers), but have no desire to really get those working in the engine. The interfaces are easy, but significantly different from DirectInput, so I'll just put that on the back burner for now.

Scenes are now composed of Playfield objects. A playfield is basically an Actor manager. A typical scene may have lots of playfields. In fact, the more the better, as they help to manage more than just what's being moved around and rendered. In the asteroids clone, there is a playfield just for asteroids, one for the player and the bullets fired, and one for particles and other short-lived actors. This separation of actors helps to manage z-ordering, collision detection, and more.

Another benefit to using playfields is that actors can now "kill" themselves. This would be akin to removing a renderable object from a scene graph. This is a huge burden off the game developer since actors can be responsible for their entire life cycle. The Asteroid>>explode method takes care of killing itself, as well as spawning new asteroids on the same playfield. And it just works, which is always a good thing.

Oh, wait! I almost forgot another great addition!

With the loss of Direct3D, I lost the D3DX libraries (which I didn't want to use anyway). This meant that texture loading was back to just plain, old bitmaps. But, I decided to code up some nice wrappers for the DevIL project. It's 95% complete. All that's missing are the D3D8, Allegro, and SDL functions, which were intentionally left out. But all 3 libraries (IL, ILU, and ILUT) are implemented, along with a helper object - DevILImage - that is useful for managing images loaded with DevIL and manipulating them.

The engine actually only uses the lowest level of the 3 libraries: IL. But the others are there if anyone would like to make use of them. Thanks to DevIL, the engine now supports an entire host of image formats. Also, Direct3D and OpenGL support for DevIL was pulled out into their own packages which just add loose methods to the ILUT library. This was done so that if you didn't need it, no need to install them.

I'm starting to move onto actual interface code now. I've also started playing with Seaside in Squeak a little, as I've done enough that I think getting some web hosting going so I can start publishing packages for others to use and help test. I'm a terrible web developer, but Seaside looks pretty neat and fun. If anyone can suggest some very reliable web hosting services, definitely let me know. Eventually I'd like to use Seaside hosting, but I think it will be a while before I'm ready for that.

Until the next update...

6 comments:

tejón said...

Forgive me if there are good reasons not to do this -- I don't know jack interface management. But I'm seriously turning on to refactoring, and my #1 Talking rule is "if you'll ever have to type it again, keyword it." :)

OpenGLPresenter>>blank
self makeCurrent ifTrue: [
(OpenGLLibrary default)
glClear: GL_COLOR_BUFFER_BIT].

Now your first example is:
p := OpenGLPresenter show; blank; flip.

The entire operation fits on one line with semicolons: I think that's a big thing to aim for. It makes the application programmer's life easier; and on top of that, the more you can hide the guts of OpenGL, the less the end programmer really has to know OpenGL in the first place.

In that vein, what's #flip? Page flipping, to refresh the display? You're probably going to type that a hell of a lot, so what's the harm in building it in to most of your methods? Certainly you don't want to flip after every operation, but when a method is a whole batch of operations, why not? The application programmer is concerned with what goes on the screen and where, but do they really need to worry about the vagaries of timing?

Excuse me for repeatedly jumping in and prodding your code with my amateur simplification stick. It's just that this is the non-macro macro functionality I've been consciously craving in a language since about the time I wrote my first line number in Applesoft. I can't get enough! :)

tejón said...

Heck, speaking of refactoring...

OpenGLPresenter>>asCurrent: aBlock failWith: aFailureHandlingBlock
self makeCurrent ifTrue: aBlock ifFalse: aFailureHandlingBlock

OpenGLPresenter>>asCurrent: aBlock
self asCurrent: aBlock failWith: [default failure handling]

...and now that ubiquitous test is not only quicker to type, but also does something!

Jeffrey Massung said...

tejón,

No worries! The blog is slowly picking up traffic and seems to be encouraging good discussion. And I'm happy to answer any questions that come up.

Regarding the creation of a #blank method. Sure, it's doable, but that's a prime example of where an assumption made by the engine won't work for all games.

Clearing the frame buffer takes time, and can interfere sometimes with the desired result. For example, a poor man's blur effect might be achieved by never clearing the frame buffer and rendering a black, semi-transparent quad over the entire display each frame instead. The effect can be quite cool. :-)

As for #flip, yes, that's used for page flipping (but not necessarily to refresh the display). And it's hardly something you'd want to factor out. It's called once per frame, from one place in code, after all other rendering is done.

Hope that answers some of your thoughts!

tejón said...

Maybe what I called #blank should be called #fresh instead. Or maybe it's not actually going to be called often enough to warrant its own method. :)

I'm just riffing off of the sample code you've shown us, I'm too ignorant of the ways of rendering to know what actual coding patterns will look like. The sentiment remains the same, though... but y'know, I don't think I can put it more succinctly than "Extend and Simplify," so I probably shouldn't keep rambling at the guy who titled this post. ;)

Oh, and I meant to say: hooray for switching to OpenGL! Among other things, that move might eventually make this package useful to non-Dolphin Talkers.

Can't wait to see it in action!

Flatlander said...

You know, your writings actually inspired me to hack some Smalltalk/Squeak for a change :)
So far I have been busy with binding HGE and Irrlicht but I'm starting to think that doing things in straight OpenGL might be a better choice.
I wonder if your work could be easily ported to Squeak, I haven't checked Dolphins FFI yet but I guess it is not too different from Squeak's...

Jeffrey Massung said...

Flatlander,

I'm glad! HGE and Irrlicht are both great packages, and I'm sure you'd be happy with either one. But, I suggest you pick a domain. What do you want to do? If you want to make 2D games, HGE is definitely going to be better than Irrlicht. 3D? Irrlicht for sure.

I don't know what the FFI bindings are like in Squeak, but I don't imagine them being significantly more complicated than Dolphin. Perhaps what I have could port over with some simple find/replace in a good editor? I don't know.

As for porting over my engine, if everything goes as planned, that could prove very difficult in the end. The concepts could probably keep as well as the low-level object hierarchy, but I have some pretty cool things I'd like to do that are pretty specific to Dolphin's MVP architecture.

Feel free to email me if you want to chat some more!