How To Cog - Chapter 3.3 - Encapsulation

Encapsulation is a general term used to describe seperating the code needed to perform a specific task into its own subroutine or object. This allows you to hide a lot of the lower-level code that you won't want to waste your time with, and it makes it easier to read and debug your code.

With Object-Oriented Programming, encapsulation takes on a new meaning. Instead of combining all of the code needed to perform one task into a subroutine, you're putting it into the methods of an object. An example of an object would be a thing like the player, and an example of a method would be a verb that can set or get a thing's property values - such as GetThingHealth().

Cog is very much Object-Oriented, but it's a scripting language that differs a lot from standard OOP in languages like C++. Cog is missing a lot of the syntax that other languages have for objects, but you can still use encapsulation to make your code easier to work with.

Encapsulation with Handlers

Say you have a cog that's about 500 lines long. The purpose of this cog is to manage an AI starfighter, and it will do everything needed to get the fighter to fly around and attack enemies.

On startup, your cog will start a pulse that needs to check the fighter's status, search for enemies, and fire at enemies. You might also want to evade enemies chasing the fighter, play some engine sounds, and depending on how good our AI needs to be, you can fire some ghost projectiles to search for walls.

The simple approach would be to put all the code for this in the pulse message handler. This would work fine, but you'd have an unreadable mess of about 400 lines for just one message handler. So what you need to do is break up that handler...

For each specific task that the pulse handler needs to accomplish, you can create a new handler and put a call in the pulse. Your message names might be fighter_search, fighter_mode, fighter_move, fighter_fire, engineSound, etc.

With the pulse code broken up into many smaller handlers, you now have a much shorter, more readable pulse message. Say the fighter isn't moving - you can go to fighter_move and look for the problem there. This also reduces the amount of redundant code in your cogs - if you have the same three commands used in many places throughout your cog, you can encapsulate those commands into their own message handler and then use a call statement.

Encapsulation with Cogs

Although breaking up large messange handlers into smaller ones makes large cogs more manageable, working with cogs that are more than a few hundred lines can still be a headache. It's no fun working on the same cog for weeks at a time and forgetting parts of the code as you try to improve it.

So what you can do is create several different cogs that encapsulate the main tasks you're trying to do. Remember the grappling hook example from the Writing Cogs tutorial? We could easily have put all of our grappling hook's code into the weap_bryar cog, and that's what most editors would do. But what if you have to troubleshoot something? It's really awkward when your code is added into another cog that is already working fine.

By giving the grappling hook its own class cog, you have encapsulated all of the grappling hook's tasks into the hook object itself - because the class cog is a property of the object. This is much closer to OOP encapsulation that anything else you can do in cog.

Another good reason to break up your cogs is when writing client / server patches. It's much easier to debug when you have one cog that contains all of the client code, and another that contains all of the server code. Multiplayer coding is one of the hardest things to do, and encapsulation will save you time.

Redundant Code

Redundant code is the bane of good programming. In any place that you have the same or similar commands, you can use loops or calls to make your code shorter. Although this is often more work for JK, making your code easier to read is well worth it.

But in some cases you'll end up with several cogs that all do pretty much the same thing - and even have most of the same code. Each cog has something unique making it hard to combine them all into one cog. You may have everything encapsulated nicely - each cog performing its specific, unique task. But what if you make an important to change to some of the code that all of the cogs use. Then you have to go to each cog and make that change - this is a huge waste of time and there's a good chance that not all of the updates make it to all of the cogs.

Say you're making some AI actors that are going to be smarter than the average bear. You don't want to just up the stats with AI files, so you're going to use class cog to do a lot of extra work for the AI. But not all of the actors you're making are going to be the same: you want humanoids, arachnids, and reptiles. All of these actors need to use the same decision process, but their weapons, models, sounds, animations, etc will all be different.

Doubtless the AI code will be the most important, and you should never have to copy large portions of code into other cogs. The best way to do this might be to move all of the AI logic into one level cog. Everything specific to the type of actor would be left in the class cog. When the logic cog decides to make the actor do something, it can use SendMessage() to tell the class cog to do it. Your code would become harder to read, but it would get rid of the redundant stuff.


Previous: Communication Up: How To Cog Next: Debugging
  • Create:
This page was last modified 16:18, 14 April 2006.   This page has been accessed 1,538 times.