Blitz2D Intermediates: Reusable Code: Portable Modules in Blitz
by morduun

What Am I Talking About?

Portable modular coding is the best way I've found to structure a program in Blitz. The methodology follows a lot of standardized advice and takes a lot of cues from object orientation. The purpose of coding this way is twofold: a consistent, logical approach to problem-solving in code, and the ultimate goal, portability and reusability. Using these methods I've been able to build up a substantial codebase for Blitz: functions I easily bring into every Blitz project I make, and this saves me untold hours of coding time.

It's important to keep one thing in mind: there is no "best" way to structure your code. Everyone's mind works differently; some people think better as procedural coders, some think best as object coders -- some people even like COBOL! But if you're getting into a large project, or are starting to get tired of recoding the same routines over and over again each time you start a new project, this approach may be at least of interest, and possibly of value.

GUIDELINE ONE: Self-Contained Modules

A module is an entirely self-contained include file. Within the module are all Constant and Global declarations, all Type Definitions and all functions associated with the module's functionality -- a zealous coder might even include a small routine demonstrating the module's use and comment it out for production. You might have a module for networking, a module for graphical user interface, a module for asset management and so forth, but the idea here is that you'll have EVERYTHING pertaining to that concept safely tucked inside the module.

What you're really building here is a class -- or at least something as close to a class as you can get in a BASIC dialect. You'll have to live without some of the advantages of true OO, but even so the approach is a very strong one when it comes to reusing your code.

GUIDELINE TWO: Naming things

When you're building your module, you're going to want a consistent naming strategy. If you're creating, say, a terrain creation library, your type name might be Terrain; if that's the case, give all your constant values, globals and function calls the TERRAIN_ prefix. It ends up being a bit more typing, but the organization it lends to your code is invaluable. You'll immediately recognize which modules are giving you errors, immediately know how to track down the values that are giving you troubles and your debugging time will drop as a result.

Doing this also leads nicely into the next topic, treating your module like an object, or class. When all of your function names are directly associated with their related type, not only will the functionality be nicely standardized (creation of any object will always be an OBJECT_Create() command, for instance), but you'll start to think of the functionality as extensions of the object. This mentality can make a LOT of coding tasks simpler, as it's often more effective to solve a problem in terms of giving a game object a behavior, rather than in the larger terms of how to accomplish an entire goal in one fell swoop.

GUIDELINE THREE: Think like an Object

Blitz Basic gives you a very powerful thing to play with: Types. It also gives you some very powerful keywords to deal with those types: New and Delete -- New creates a blank type instance, and Delete destroys it.

You must never, ever use these directly in your game code. Ever.

Why? Simple really; the cases where you =want= a new type instance to have fully zeroed-out values tend to be few and far between. Initializing the data is what comes next after you create a new type instance, and if you're calling the New keyword then you're probably filling in the field blanks next.

Imagine you do this, inline, in your program, say, ten times. Bullet creation routine, say, that gets used by players, AI opponents and a few installations. Now imagine that, as you're giving buildings the ability to fire weaponry, you realize that the data structure of a bullet needs to change. You're now going to enjoy going through all your code -- all of it -- finding all the spots you create a New bullet instance, and changing the entire code to fit the new data structure.

Now, imagine you've instead created what's called a constructor function for bullets. Instead of creating a New instance inline and manually filling in the blanks, you have a BULLET_New() function that returns a new bullet instance. You pass the parameters via the function call, and the constructor function does all the building based on the arguments you pass to the function. If the data structure changes, you only have to change the way the function applies those arguments to the new data structure -- your inline, external code doesn't have to change at all. One change, in a place you can quickly identify, versus ten changes that you have to search for individually -- the benefits should be pretty clear.

In the other direction, the Delete keyword should also never be called inline. As you get deeper into using Types, you'll often have assets or other media associated with your Type instances. Deleting a Type instance without freeing its related assets first will result in a memory leak: the reference to the asset is gone, but the asset remains loaded in memory. Sure, you can handle all this inline, but you'll need to modify those inline references should the type definition change, or more assets be loaded into new type fields. By creating an OBJECT_Destroy() function for all your modules, and by coding tests for assets (and freeing them appropriately), you can happily destroy type instances at will without worrying about whether you're bleeding memory or not.

GUIDELINE FOUR: Prototype your Objects

Blitz has no real means for inheritance -- that is, deriving functionality from parent types. What you can do, however, is prototype by using two Type definitions: one for the prototype, and one for the in-game instance. You might develop a library of enemies in prototype, storing things like their name, display entity or image, movement rate, fire rate, loaded sound effects, bullet types and maximum health in the prototype. When you then create the actual in-game instance, you link the in-game instance to its prototype, storing only instance-relevant information in the in-game instance -- sound channels, copied entities, current health, heading and position, and so forth. In this way you can create generic movement, ai and firing code, all data-driven by the prototype information. It also becomes child's play to add new monster types: add a new monster prototype and the generic routines will do the rest.

Going this route is a precursor to making your game data driven -- an extremely good thing but beyond the scope of this tutorial. A data-driven design allows you and your players to easily modify your game, trade levels with each other and extend the playtime so long as there is interest in the basic engine.

GUIDELINE FIVE: Key Routines

Every module should generally have at least three routines:

OBJECT_Create.object()
OBJECT_Destroy(this.object)
OBJECT_Manage()

The first two we've already gone over; the third is key for manipulating objects within the context of your game. The management routines should be called every loop; in general I find that passing them the current game time is a nice way to handle management routines, but if you store that in a global then you can simply call it without arguments. It should iterate through each Type instance and call functionality as appropriate to the object's current game state -- don't inline all the code, just call the routines as the game object's state requires.

Most of the time I also code up an OBJECT_GetByID.object(ID$) -- just a simple routine to iterate through all type instances and return the instance with the given ID. I'll also code up supporting functions if appropriate -- OBJECT_GetByLocation.object(x, y) and OBJECT_GetBy

GUIDELINE SIX: Always Return a Value

No, really, I mean it. If you have a constructor function, it needs to return the new type instance, but what about the destructor function? Why do I need to return a value there?

Because it helps you debug. Blitz fails silently if you try to destroy a null type instance -- but if you're trying to do that, there's something very wrong elsewhere with your code. By having your Destroy function return a False value when you try to pass it a Null type instance, it's one more clue to help you track down where things are going bad in your code. Management functions can return how many objects they've processed, and just about any routine can return a true or false based on whether the operation succeeded or not. If you intend to share your routines with others, this is absolutely a requirement.

It's just a little more code, and the hair you save may be your own.

GUIDELINE SEVEN: Functions: One Screen Only

You've probably heard this one before: if you're going to code a function, keep it to one screen of code. I'm going to repeat it because it's worth repeating: if your function is over one video page of code, there's no way for you to effectively modify or debug it, especially over time as you reuse code -- remember, part of the point of this exercise is creating code you'll use over and over again! If you realize you need to debug something six months after you've written it, you want it to be as simple as possible.

If you find yourself going past this limit in a function, examine the functionality, work out what bits are especially complex or intensive (or, even better, common to other routines), and move them into other functions. There =is= a slight overhead for calling functions versus having inline code -- but I've never found it to be so much a drain that it makes the benefit of readable code worth sacrificing.

Final Notes

There's a file to check out that demonstrates most of this (with the exception of prototyping, which I'll get into if there's enough interest in a future article on data-driven engine coding) -- it's the mordypong file, and as you might expect it's Pong -- but it uses the concepts in this article to drive home what I'm talking about. Grab it here, then dissect it, play with it and come back here to talk about it.

The goal of this article is to help everyone reuse and share code better -- I hope it's at least given some inspiration to that end. Go ahead and use this thread in the forums to talk about the concepts and ask questions, or email me at morduun@gmail.com if you like.


For a printable copy of this article, please click HERE.


This site is Copyright© 2000-2004, BlitzCoder. All rights reserved.