In Dark Forces, Kyle Katarn, a young mercenary successfully infiltrated the Empire. Jedi Knight continues the story of Katarn as he embarks on a quest into his past and learns the mysterious ways of the Jedi. With this knowledge, he must stop seven Dark Jedi from unlocking the powers of a hidden Jedi burial ground. This task forces Katarn to decide his destiny. If he chooses the Dark side, he will come into enormous power. If he chooses the Light side, he faces seemingly insurmountable evil. Whatever path Katarn chooses will change the face of the galaxy forever.

Post tutorial Report RSS DataMaster's Cog scripting tutorial: Chapter 3

This is Chapter 3 of a Cog scripting tutorial for Jedi Knight originally posted to Saber's Domain. It goes over some of the advanced subjects.

Posted by on - Advanced Client Side Coding

This is part 3 of Saber's domain's cog scripting tutorials. It has been mirrored here for archival purposes. This is the final part. You can find sample scripts, programs and further documentation here. You can also find parts one and two on moddb.

Introduction


This chapter won't be in any particular order. It's just a collection of advanced topics gathered into a chapter. Some of these topics are on a much different skill level than others, but you should read them all after you've had a little experience with cog.

Communication


In General

Whenever you create a new cog and add it to a patch, it will be completely seperate from other cogs. There's no mistake you can make that will cause an error in another cog. But when you need your cog to act in response to other cogs, you're going to need some way to communicate.

Cog provides several ways to do this, but no matter what method you use, using several cogs to do something makes debugging and just understanding your code much harder. If any of you have tried to make sense of Raynar's Rbots code, then you know how much more complicated your code can become when you have to break up one cog into several smaller ones. Always be sure to write in plenty of commenting beside verbs like SendMessage() and SendTrigger().

Sending Triggers

Triggers are the hardest, most professional, and perhaps the most taxing way to get comm between your cogs. A trigger is sent out with SendTrigger() and received with the trigger message. The syntax is:

SendTrigger(dest, ID, param0, param1, param2, param3);

For what we're going over, the destination is the most important argument. This tells the verb who the trigger is going to be sent to. The integer is either going to be -1 for everybody, or the thing number of the player who's computer we want to talk to. We'll go more into this in the multiplayer tutorial.

If a trigger is sent out with a dest of -1, then every cog on all computers in the game will be able to receive that message. Triggers wouldn't be that useful if you couldn't tell them apart - that's what the second argument is for. This is the trigger's ID, which will be the sourceref of the trigger message.

The fact that all cogs that use triggers have to listen to every trigger makes them somewhat wasteful. In most cases, you'll have one cog that needs to tell something to another cog. If you use a trigger and a unique ID, every cog on the receiving computer will have to look at that message and compare it's ID to the IDs that it's looking for.

Also, because triggers have only a player as a destination and not a cog, it can be difficult for anyone reading your code (and for you later on) to tell which cog is listening for that trigger without any comments.

For example, let's say we're writing a new scoring system and we need to write something that will make the server-side cog send a trigger to the client-side cogs to tell them what the new score is. The server code might look like:

broadcast_score:
 // tell all clients that the new score is curScore.
 // receiving cog will be client_score.cog.
 SendTrigger(-1, 33, curScore, oldScore, 0, 0);

return;

And in the client's cog we would have:

trigger:
 if(GetSourceRef() == 33)
 {
  // received trigger from server_score.cog.
  // first param will be the new score, and the
  // second will be the old score.
  curScore = GetParam(0);
 }

return;

In cases like this, there are an unknown number of recipients, verbs like SendMessage() are designed to send a message to a specific cog - and they can't do messages to other computers. So for this example, SendTrigger() is the only choice even though only one cog needed to receive the trigger.

Sending Messages

For sending messages, the two verbs we'll use are SendMessage() and SendMessageEx(). The syntax looks like:

SendMessage(cog, message); 

retval=SendMessageEx(cog, message, param0, param1, param2, param3); 

SendMessage() is the simple version, it just needs the destination cog, and the message to send. SendMessageEx() is the extended version which allows you to send four parameters.

Since an ID is not used when sending messages, you have a range of user messages (user0-user7) to use. There's also a global0 message which you can use if you need to. You aren't restricted to using only these messages, you can use any of JK's predefined messages.

The downside of these message verbs is that it's hard to get a reference to a cog - it's not the cog file, it's the number of the cog in the JKL's cog section. A common way to add custom cogs to a patch is through the items.dat. This avoids using the static.jkl, and it allows you to use GetInvCog() to return a reference to that cog. So if you load all of your cogs through the inventory, then you won't have a problem sending messages between them.

For an example, let's say that you have two custom cogs. The first one needs to synchronize three variables with the second. If we used a trigger in this case, then every local cog would hear the trigger, and we don't need to do that. So the first cog might have code like:

comm_sync:
 SendMessageEx(GetInvCog(GetLocalPlayerThing(), 117), user0, syncVar1, syncVar2, syncVar3, 0);

return;

We're assuming that the first cog is loaded with bin 116, and the second is loaded with 117. The second cog would have this code:

user0:
	syncVar1 = GetParam(0);
	syncVar2 = GetParam(1);
	syncVar3 = GetParam(2);

return;

Using the Inventory

In that last example, we could have used bins 118 to 120 to hold the three variables. It would depend on whether we needed the second cog to act immediately on the change in variables or not. Bins can act as global variables - variables that every cog can modify without making a local copy. This would allow both cogs to change and use the variables as they needed without talking to each other. If you have bins to waste, this might be a better option.

Message Parameters

These parameters are made to accomodate most variable types, but vectors, being as large as they are, have to be broken down into three flexes before you can send them. If you've got a few vectors you need to send (say you're trying to sync something's position and movement), then you're going to have to use multiple triggers. Most of the time it's better if you find another solution than to try that.

As you saw in the syntax of SendMessageEx() you can send a value back to the calling thread with ReturnEx(). The return type is flexable just like the other message parameters. ReturnEx() is used the same as in damaged and autoselect.

Encapsulation


Definition

Encapsulation is kind of 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 for you to read and debug your code.

With Object-Oriented Programming, encapsulation takes on almost a whole 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 in Jedi Knight gameplay, and an example of a method would be a verb that can change the properties of a thing.

Cog is very much Object-Oriented, but it's a scripting language that differs a lot from standard OOP as in C++. So if the above description helps you to understand objects and methods, that's great. But be aware that true OOP languages have a lot of new syntax for objects and their methods.

We may not be able to encapsulate our code in objects, but we can still use message handlers and even whole cog files to do the same thing. Just remember that even though the concept is the same as with other languages, the implementation is a lot different.

Encapsulation with Handlers

For this example, we're not going to deal with real code, but let's assume that we have a cog that's about 500 lines long. The purpose of this cog is to manage an AI starfighter - this cog will do everything needed to get the fighter to fly around and attack enemies.

On startup, our cog will start a pulse that needs to: check the fighter's status, search for enemies, and fire at enemies. We might also want to evade enemies chasing us, play some engine sounds, and depending on how good our AI needs to be, we 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. But that would be an incredible mess. We'd have one message that's about 400 lines long. This makes it really hard to read and debug your cog. So what we need to do is break the cog up...

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

With the pulse code broken up into many smaller handlers, we now have a much shorter, more readable pulse message. If, for example, the fighter isn't moving, we can go to the 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 our 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 on to another cog that you already know is working fine.

By giving the grappling hook its own class cog, we 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 your writing client / server patches. It's much easier to debug when you have one cog that contains all of the client-side code, and another that contains all of the server code. Multiplayer coding is definitely one of the hardest things you can do in cog, and using proper encapsulation will save you some time.

Redundant Code

Redundant code is the bane of good programming. In any place that you have the same or similar commands, you use loops or calls to make your code shorter. Although this is more work for JK, making your code easier to read and debug 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. But each cog has something unique that it does so that you won't be able to just have 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 - and that's a nightmare.

Let's say for example, that 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. So to fix this, there's a few things we can do. First, we could remove the actors' class cogs and create one level cog where we could enter in the different resources that you know the actor will use. But if we need to keep the class cogs, we could create a new AI logic cog and have the class cogs send and receive user messages - all of the redundant code would now be in one place, but the comm between cogs could be a problem. It depends entirely on your situation.

Debugging


In this tutorial, we're going to go over the basic decision process of debugging something you've written. Once you get used to it, this will all seem pretty easy, but it takes most editors a while to learn how to debug their code. Too many beginners go straight to a message board or chat room because they don't know how to debug properly.

Print Verbs

The print verbs that JK provides are probably the most helpful debugging tool. If you've just typed out a small cog and nothing's happening in the game (definitely the most common problem you'll face), put a print statement in the startup handler to make sure your cog is actually running. Remember that you'll need to use a sleep of about two seconds depending on how long your machine takes to start the game after startup.

Although Print() is the most common, there's a ton of Print Verbs available to you. These verbs are pretty self exaplanatory, so I don't want to write a tutorial just for them, but they are very useful for debugging so I'll squeeze them in here.

Concatenation means to link or join together IRL. In programming, concatenation (or just concat for short) is the term we use to describe adding strings together. In some languages, the '+' operator is overloaded (given another purpose) to concatenate strings. So you'd be able to write: stringVar = 'word1' + 'word2'; And stringVar would have a value of "word1word2".

But we're not that fortuneate with Cog. Instead, we're given the same functionality with the concat verbs. JK keeps a string set aside in its memory for use with these verbs. And this string is not cleared unless you clear it. So the first thing you do when you concat strings is to clear whatever was left over from the last concat operation. To do that, we use JKStringClear().

We can concat ASCII strings, flexes, ints, players' names, UNI strings, spaces, and vectors - we're given a lot of flexibility. Concatting (author's terminology, not to be repeated. :) ASCII (think of this as a simple computer alphabet) strings are entered as strings, e.g., JKStringConcatASCIIString("yourTextHere");

For flexes and integers, you can either concat them the same way the regular print verbs display them, or you can use the ConcatFormatted verbs which allow you to use format specifiers - these give you a lot of options for customizing how the numbers will appear. Format specifiers aren't part of Cog, but of Visual C++. The Print Verbs notes includes a complete description of the specifiers that Cog can use. The best use of these specifiers is to output numbers in hexadecimal - very useful for checking flags.

The Concat verbs are the only way to retreive a player's name, but there's not much you can do with it except to print it. This isn't helpful for debugging - just for displaying scores. Concatting a UNI string is pretty useless because there's already a UNI print verb, and there's not any reason to want to concat a predefined string.

Concatting a space is also pretty useless because you can do that with the ConcatASCII verb. Concatting a vector is very usefull for debugging when you need to look at several vectors at once. If you just printed them out, they'd all appear in the same column and you wouldn't be able to tell them apart. But with the concat verbs, you can print as many as you need in one line.

Results of Print

So your cog's not working right, and you've just tried putting a print in the startup handler. If nothing printed (and you had a sleep of at least two seconds), then first make sure you correctly declared the startup message. If that's there, then you need to make sure that your cog is being loaded. If your cog is correctly listed in either the items.dat or the cogs section of a JKL, then you must have a syntax error.

The most common syntax error is a missing semicolon somwhere in your cog. Or maybe you've got a missing code or end keyword. Sometimes the error will be subtle - like having spaces in the flags assignment - the cog looks fine, but when JK parsed (read through) the cog, it found something it didn't expect and this caused it to read in garbage.

Now if the print did work, then you know the cog is being loaded and your syntax is (for the most part) correct. If you ever see the print appear twice, then your cog is being loaded twice - check for duplicate entries.

Once you're done with that basic troubleshooting step, how to proceed depends on your situation. Say you have a certain message handler that's not running. You've put a print in that handler but nothing's printing. It's probably an event message for an event that's not occuring (or the cog is not associated with the object(s) it happened to).

If you know for a fact that the event did occur to an associated object, then the cog may be blocked from receiving that message. Mask flags are the most common cause (the defaults are very restrictive). In a class or inventory cog, you can do nothing; the only way to change mask flags is to use a level cog and the mask symbol extension to allow any sourceref for that event message.

They key to effectively solving problems with portions of your code not working, is to start by putting print commands at startup and work down to where the problem is. Once you find the point of no return in your code, check the surrounding syntax - you might have missed something easy like a semicolon right after an if statement. If it's not obvious then check your references for any comments on the verbs or keywords you're using. If you have questions about a verb or its syntax, most of the time it's a common enough question to be in a reference somewhere.

Flow Control Errors

Flow control is a phrase that programmers use to describe how code execution flows down through your code. Sleeps, Calls, Pulses, Loops, and If-else statements are all methods to allow programmers to change the normal top-down flow control of a program or script. Normally, an event is received by a cog and code execution starts right after the label and ends when the return command is given.

A Sleep() pauses code execution, but it's a lot more complicated than that because JK has to remember where it stopped in the code and what it was doing at the time. In the Threads tutorial, this is gone over in a lot more depth, but basically you have a limit on how many sleeps you can have simultaneously. Putting a sleep in a pulse handler (which is always a bad idea) where the sleep is longer than the pulse interval will result in what the author will call a thread overflow - basically JK is trying to run the pulse handler, but it already has so many paused code executions in memory that it has no more room to create more. Timers do not have this problem and should be used instead of sleeps if possible.

Calls are similar to sleeps in that current code execution is paused while JK calls on another handler. If you nest one call inside another so many times that you hit the thread limit, the oldest thread will be deleted. So code execution will never resume in the handler that made the first call.

Loops, in all their different forms, have a block of code that's run, and a condition that's evaluated to see if that code should be run again. If the condition is true, then the flow of code execution resumes at the beginning of the loop's code block or statement.

If statements, especially ones that are deeply nested with elses, can also be a source of errors if you don't use proper indentation. Flow control can be difficult to imagine as you're reading your code if you can't clearly see which else belongs to which if.

To avoid errors like these, try to keep in mind how JK will read through your code and make decisions instead of just how the code reads to you. Some operations that seem simple to read might have a lot of hidden overhead that JK nicely hides from us.

Multiplayer


Introduction

Multiplayer coding is one of the hardest things to do in JK. The code itself isn't difficult to write - the hard part is making sure your cog is running in sync on all computers. Most well-designed multiplayer cogs use a client / server system so that one cog is designated to keep the other cogs in sync with it.

Multiplayer Flags

To control how your cogs will run in multiplayer games, there are several Cog Flags you can use. Take your time to read up on these flags - remember that cogs have default flags, and each type of cog has its own defaults. The main flags we'll use are 0x40 and 0x200. 0x80 is also important, but we're not going to cover MotS.

Jedi Knight will sync a lot of things in the game. No one knows exactly what it does sync, but there are obvious things like the player's position (btw, if the player is not significantly moving, the game won't sync his pos). That type of syncing is stuff that the engine takes care of, cog has nothing to do with that. But other things like thing creation and changing models are synced with other computers only because the verb (FireProjectile() and SetThingModel()) will take care of the syncing for you. Not all verbs will do this, and some verbs don't need to because JK is already syncing whatever it changes.

Because multiplayer testing requires a few computers, and preferably a few people, not that much multiplayer research has been done. So you're pretty much on your own for figuring out what JK syncs and what verbs do the syncing for you - usually you can make a good guess and you'll be right.

What the 0x200 flag does is it stops verbs from syncing their changes with other computers. So if we have a cog that's going to run on every client, we don't need it to waste time and bandwidth trying to sync changes with other clients that are doing the same thing. So we'd add 0x200 so that each client cog will only make changes locally.

By default, cogs run only on the server - in singleplayer, the computer is assumed to be the server. Class and inventory cogs are given the 0x40 flag by default so that they'll run locally on every computer - client and server. This is needed because your weapon cogs, for example, would be useless if they only ran on the server's computer.

A combination of both of these flags (0x240) will make a cog run locally on every computer and not sync its changes. Most multiplayer cogs are given this flag. But remember, if you have a simple server cog that needs to sync its changes (and the verbs you're using can do it), you won't be using either of these flags (0x40 is useless to the server anyway).

There is no good way to find out what flags your cog has in JK - there's no verb to do it with. The only way we can guess at the purpose of JK's flags is to test in MotS, and since MotS is slightly different, there's not a good way to make sure the flag info's accurate.

Client / Server Versions

Latency is the term used to describe the time it takes for one computer to send information to another. Most people just call this "lag time." High latency can cause a lot of problems for our code because the goal of multiplayer cogs is to sync information between computers. And when we have bad latency (depending on how much we need to sync), the difference between the server's information and the client's can get bad enough to make your patch unplayable.

Let's say for example that you have exploding barrels that are created on a conveyor belt. This conveyor goes to a bunch of places in your level. One of the things that JK doesn't sync is throwable objects, so if we want our barrels to be the same on all computers, we're going to have to do that manually. And to make it harder, we want players to be able to score by killing other players with the barrels' explosions.

This kind of example is going to require a lot of communication, and if the barrels are not in the same places on all computers, then one player may see a barrel blow up right in front of his target (because explosion effects are local), but on the target's computer, the barrel may be way too far away to damage him - he'll see the explosion over there.

That is how latency causes problems for us. Suppose we have a latency of 200 milliseconds - this would be 56k dialup connection. This may not seem like much time, but since our barrels are on a conveyor, we're going to see the barrels hopping all over the place as clients receive old positions from the server. You'll see the client's barrel move smoothly down the conveyor, and as soon as your cog receives a trigger with the new position, you'll see the barrel hop somewhere nearby.

And it gets worse. If you're only syncing the position, the barrel may be pushed off the conveyor on a client, and it's movement will be stopped. But the server will still send it position updates according to the server barrel's movement. Barrels can move a lot in a fifth of a second, so that could look really bad.

So although we had a good idea and it can work pretty well, you're going to have to explain that your patch won't work over the internet on slow connections.

Communication

The different methods of communicating between cogs are covered in the comm tutorial, but the only one we're concerned with here is triggers. Briefly, triggers are sent out to one or all computers with SendTrigger() and received with a trigger handler. Triggers are sent with an ID so that you can tell them apart. They're our only option for multiplayer comm, but they're not too bad.

Make sure you put plenty of comments beside your triggers. Although we've talked about client-side and server-side code as being in seperate cogs, you can easily put them in the same cog, and many editors do. Take a look at this example:

pulse:
	// pulse is run only by the serving computer.

	for(i=0; i < maxBarrels - 1; i=i+1)
	{
		// just to make the code easier to read.
		pos = GetThingPos(barrel0[i]);
		// send out the trigger to all computers (us too).
		// can't send a whole vector, so we'll break it up.
		SendTrigger(-1, 35, VectorX(pos), VectorY(pos), VectorZ(pos), i);
		// we don't want to flood our network with
		// triggers, so sleep for a very short while.
		Sleep(broadcast_interval);
	}

return;

trigger:
	// if this is a trigger this local cog is sending, ignore it.
	if(GetSenderRef() == GetSelfCog()) return;

	if(GetSourceRef() == 35)
	{
		// receiving a barrel pos update from a server.
		// first, rebuild the vector.
		pos = VectorSet(GetParam(0), GetParam(1), GetParam(2));
		// now set the local barrel's pos.
		SetThingPos(barrel0[GetParam(3)], pos);
	}

return;

Something not obvious in the example above is that the thing numbers of things created in the game after startup will not be the same on all computers. That's why we have to keep an array of all of the barrels we create. And when we send out pos updates, the index number is sent. So as long as we make sure our arrays are set up correctly, there won't be a problem.

This example for the scenario we worked with earlier shows how a cog must be prepared to be run on several different computers. Only the server will use the pulse message, and it will ignore updates that it sends to itself. However, all of the clients will respond to the trigger and reset their barrels' positions.

Our example's cog is assumed to have the 0x40 flag which must be added if it's not going to be an inventory cog. Cog does have a syncing verb for positions, but it doesn't just set the new position - it tries to move them to their new position sometimes making the situation worse.

Threads


Definition

A thread is the standard programming term for the flow of control through code. If you're familiar with operating systems and multi-processor systems, then you understand that PCs with more than one processor are capable of running two threads simultaneously whereas systems with one processor can create the illusion of multi-threading, but they can't actually run two threads at once.

Of course, the threads of a scripting language like Cog aren't going to be the "real" threads used by compiled languages, but they are similar, and we can borrow the terminology.

How They Work

We've said before that when a cog receives an event that it has a handler for, JK finds that handler's label in the code section and code execution begins. But what if that handler contains a sleep and another message is received before the sleep ends? And what if it's the same event?

Threads are the answer to that problem. Cog is capable of having multiple threads for one cog, but JK will only work with the last-created thread. Here's a coding example that's designed to work with the above scenario:

activated:
	if(on) return;
	on = 1;
	channel=PlaySoundLocal(yourSnd, 1, 0, 0); 
	Sleep(GetSoundLen(yourSnd));
	on = 0;

return;

So in our example gameplay, a player activates a console and the above handler is run. The handler first checks to make sure it finished playing the sound from the last time it was run. Now if the player activates the console twice in succession before the sound finishes, the variable on will still have a value of 1, and the second thread will die with that return statement.

You can have up to five threads for each cog. Threads are local to their cogs - meaning that if one cog has five threads, it won't stop another cog from having its five threads. Whenever a cog needs to create a new thread in response to an event, the oldest thread that the cog has will die - or in some cases the new thread will not be created (as in a call).

Let's say that you have a lot of calls to carefully encapsulated messages in your code. But you've noticed after some recent changes that some of your calls aren't working. You need to check and make sure that you don't have more than four calls going at once (remember that the original message handler has a thread too).

Be careful when using sleeps that the sleep's handler won't be run again before the sleep ends. Otherwise, the thread limit will be exceeded, and you'll have to wait for the sleeps to end before you can do anything. And because when JK looks at the cog to see if it needs to do anything (like wakeup from a sleep), it will only look at the last thread. Now if the cog's first four threads slept for only a half second, but the fifth thread slept for five, the cog would wait those five seconds before going back to the first four threads.

If for some reason it's impossible for you to avoid too many threads building up or indefinite sleeping, JK provides the Reset() verb to kill all threads except the current one. This can be a good safeguard, but if your code is well-written, you shouldn't have to use it.

Affected Verbs

We've just gone over how calls and sleeps can cause thread problems, but any verb that causes another thread to be created in its cog can cause the same problem.

When you use SendTrigger() to send a trigger out, this event message is sent out to all of the local cogs that are listening - including the cog that sent the trigger.. Whether that cog's trigger handler is looking for that sourceref or not, a thread is still created for the handler - if you've already got some sleeps or calls going, two more threads (the thread that ran SendTrigger() and the triggered thread) may take you over the limit.

This is true of any verb that waits until all local cogs have had a chance to answer its event message. If you use CreateThing() in the class cog of the thing that's created, or if you use SendMessage() to send a message within a cog, you'll have the same problem as with SendTrigger().

Alternatives

Instead of creating extra threads and waiting on them, you should use timers and pulses when you can. Timers and pulses are very similar in implementation and neither of them takes up a thread while they're waiting to run. Although sleeps cause a lot of problems, they aren't unstable or error-prone.

The above example, makes good use of a sleep, and there's no reason to switch to a timer. But in most cases, there's nothing stopping events from happening almost simultaneously - so even if you have a sleep of a tenth of second, there's still a chance of hitting the thread limit.

Imagine that there are ten fuel barrels in your level, and you're standing not too far away with a bryar. A few shots from you and all of those barrels will blow within a tenth of a second. If the barrels' class cog has a sleep for the slight pause before the explosion, then not all of the barrels will explode correctly. But if timers are used (and you can have a ton of timers), then everything will blow up nicely.

Jedi Knight Flags


Introduction

This is just going to be a small tutorial focused on flags and hexadecimal. For a tutorial that covers binary, octal, decimal, hex, and the related math, read the Numerical Bases tutorial. Here, we're only going to cover the basics of flags.

Bits & Boolean Values

A bit is the smallest unit of storage that computers use. It holds either a 0 or a 1. Remember from the Variables tutorial that the Boolean type needs only a 0 or 1 to hold a true or false value. Cog doesn't have a boolean type, and even if it did it would take up more than one bit.

So what we'll do is use all the bits in a regular integer to hold these true / false values. Cog's integers have are 4 bytes long - that's 32 bits and boolean values we can store with one number.

Cog programmers commonly refer to a number used for boolean values as a flag. Flags aren't restricted to any base (e.g., decimal, hex), they're just integer numbers.

Before we get to hex, go to the Flags Section of the DM, and look at a few pages. The numbers are given in hex, but the point is that each line in the table is a true or false value that requires one bit to be turned on or off in the flag that we use.

Hexadecimal

Hex is a base for displaying a number. The number itself is just an idea of how many things are there. To write or speak a number, you have to limit yourself to a base. Base 10 is what we use, but computers use Base 2 (binary). Hex, which is Base 16, is easily converted to and from binary, and it's more readable if not easier to learn.

The reason hex is easily converted to binary is that each digit in hex can display any combination of four digits in binary. This is because binary displays 2 values per digit while hex can display 16 (notice that I have to use decimal to write these numbers).

Here's a chart I'm borrowing from the bases tutorial:

Decimal	Binary	Hexadecimal
0	0	0
1	1	1
2	10	2
3	11	3
4	100	4
5	101	5
6	110	6
7	111	7
8	1000	8
9	1001	9
10	1010	A
11	1011	B
12	1100	C
13	1101	D
14	1110	E
15	1111	F

You can see that hex uses letters after it runs out of numerals to use. Look at 1, 2, 4, and 8 in that table. Notice that each number uses only one bit. This means that we can add any combination of these numbers, and no matter what it is, we can disassemble it back to what we added up. E.g., 1 and 8 make 9. You can only subtract each of our key numbers once, so: 9 - 1 = 8. C - 4 = 8. Try it on your own.

Although we can write flags in decimal, bits don't evenly fit into decimal's digits, so doing simple addition gets complicated. Let's say we have the flag 0x80F. The 0x at the beginning means that the number is being displayed in hex. This is important because if you type a hex number without it, JK won't know it's in hex and will mistake it for decimal or a variable name.

But back to 0x80F. An F means that the first digit has all four bits on - these would be listed as 0x1, 0x2, 0x4, and 0x8 in the Flags Section of the DM. The third digit only has the the last bit turned on, and it's 0x800 by itself.

Using Flags

There are many different types of flags in JK. Each type has a name like Actor Flags, Thing Flags, Physics Flags, etc. Let's use Thing Flags as an example. These are flags which are assigned to players and they're just store settings that relate to every type of thing that JK works with (e.g., weapons, powerups, players).

These flags can be assigned with the verb SetThingFlags() and cleared with the verb ClearThingFlags(). SetThingFlags() doesn't reset the flag, it just adds to it. You must use ClearThingFlags() to turn off bits in the flag. If you need to retrieve the value of a flag, you'd use GetThingFlags().

And that's pretty much it for this tutorial. You don't have to know that much about bases to be great with flags. If you want to learn more though, read the bases tutorial.

Script Parsing


Introduction

Parsing is a term used to describe how computers read data from a file. Typically, a program will open the file, and then read it line by line or character by character into memory. Sometimes the program won't need to store the file, so it will drop one line as soon as it reads the next.

We can't say for certain how JK reads cogs, but most parsers are pretty standard, so we'll go over the simple things that will help you understand how cogs are read. At least then you'll know how Parsec works. :-)

Most parsers will ignore extra whitespace (spaces, tabs, and CRLFs which is a combination of two characters: a carriage return and a line feed). But JK is very strict about whitespace until it gets to the code section. I.e., you can only have one symbol declaraction per line whereas you can have as many statements as you want on one line.

Most parsers work by looking for keywords. They seperate keywords with delimiters - these are special characters like operators, colons, semicolons, and the whitespace characters. So at any given line and character in the file, the parser will look at a string of characters from its current position until the next delimiter. If this string matches a keyword that the parser is looking for, then depending what keyword it is, the parser will look for whatever is supposed to come after it.

Header and Flags

The header is, of course, all comments. As soon as the parser sees that pound sign, it's going to ignore the rest of the line - it doesn't matter what comes after it.

At this point, JK is looking for two things: the flags keyword or the symbols keyword. So JK is going to look at the beginning of each line these words. It's taken for granted that if there's a space or something before the keyword, JK will look for the keyword after the space.

Once the parser finds the flags keyword, it's going to look for the assignment afterwords. Now in most cases, spaces can be put in anywhere, but JK is very picky about spaces that come after the flags assigment - if it finds any, it will consider it a syntax error because it doesn't expect them to be there.

This is probably because JK doesn't care about making the flags line work like a real assignment so it's search for "flags=" and it's taking from the seventh character to the last non-space character as the assigned number. Once it sees that the number's not valid - because there's a space in it, it halts and the cog is not loaded.</p> <p><strong>Symbols Parsing</strong></p> <p>After the flags, JK's parser looks for the symbols keyword. The parser will go line after line until it reaches the end of the file looking for this keyword. If it doesn't find it, there's a syntax error and the parser stops.</p> <p>When it does find symbols, the parser now has a new list of keywords to look for. These are the symbol type names and the "end" for the end of the section. If it finds something else, it will probably try to ignore it and go on to the next line, but if it can't (perhaps you forgot the end keyword) it will either reach the end of the cog or assume there's been a syntax error.

Once the parser sees a symbol type (e.g., message) it's going to look for the name, and then any extensions which might come after it. The only delimiters the parser is allowing at this point are spaces and of course the end of the line (which is a CRLF).

Code Parsing

After finding the end for the symbols section, the parser will be looking only for the code keyword. Anything else has to be an immediate syntax error. But once inside the code section, the parser acts much differently. It will first be expecting a word followed by a colon, but once it finds that first label and until it finds the end of the code section, it won't be looking for a few specific keywords.

Although we can't know for sure what JK does, it most likely takes in the first word and then looks at the next non-whitespace delimiter. If it's an equals sign, then the parser can tell that this is an assignment and the string that came before it must be a variable name. If we were doing syntax checking, we'd have stored all of the variables names read in the symbols section and then we'd know if this variable has been declared.

After the equals sign, the parser is expecting something that returns a value. This could be a function, a variable, a number, or a math operation. The parser will read characters until it finds a delimiter it's looking for. If it finds a opening parentheses, this must be a function, so the parser will then look up the function and check for the parameters it's supposed to have.

If the parser finds a delimiter that's a math symbol then it knows there's an operation that needs to be performed, and there could be numbers or variables in there. For error checking, we could be checking the type of the assigned variable to make sure that it matches the return type of the function. Or in the case of the operation, we could check to make sure all of the variables were of a type compatible with that operation. E.g., you can't multiply two strings.

The reason we went over assignments first is that pretty much any type of code can be put somewhere into an assignment. The main thing to remember about code parsing is that a parser will usually look to the next delimiter to find out what the previous string is. And statements are always ended with a semicolon so that the parser will know that it's just read a complete statement. After the semicolon, parsers will be expecting either the beginning of another statement or the end of the line.

JK's parser goes through all of the code like this until it finds an end for the code section. At this point it stops, there's no reason to look after this because nothing's supposed to be there - it doesn't matter if there is.

Timers & Pulses


Introduction

As you become familiar with Cog, you'll need to learn how to use timed code to create your effects at a later time than the present. With normal programming, this idea would never be considered - when an application needs to do something, it will perform the action as soon as it can. But with a virtual environment, you'll need delays in your code to make effects seem more natural or continuous.

For creating a delayed effect, Cog provides timers. These are special events which are triggered when a countdown timer expires. For creating a continuous effect, Cog provides pulses. Pulses are much like timers, but pulses keep going - after the pulse countdown timer expires and the pulse event occurs, the timer is automatically reset.

Using Timers

The verbs you'll use to set a timer are SetTimerEx(), SetTimer(), and SetThingTimer(). SetTimer() is the simple version - it has one parameter for the delay. You would use SetTimer() if you're only going to have one timer at a time. SetTimerEx() allows you to give your timers an ID and two parameters along with the delay. And SetThingTimer() works much like SetTimer() except that you're setting a timer for a thing instead of setting a timer for a cog.

As soon as a timer is set, JK will start a countdown for the timer's delay. When the countdown ends, the timer is said to expire and a timer event message is sent. If this is a simple or extended timer, then only the cog you set the timer in will receive the message. If this is a thing timer, then all of the thing's associated cogs will receive the message. For extended timers, you can retreive the ID and parameter values with GetSenderID() and GetParam(), or if this is a thing timer, you can use GetSenderRef() to find the thing that sent it.

If you need to stop an extended timer before it expires, you can use KillTimerEx() - the one parameter it needs is the timer's ID. Timers set by SetTimer() and SetThingTimer() need to be stopped with SetTimer(0) and SetThingTimer(thing, 0) respectively. As an example for using timers, we'll look at LEC's xtank1.cog:

damaged:
   barrel = GetSenderRef();
   damage = GetParam(0);
   barrelhealth = GetThingUserData(barrel);

   if(GetParam(1) == 1) Return;              // barrel won't be damaged by damage type impact

   if(barrelhealth <= damage)                // is this enough damage to kill the barrel ?
   {
      SetTimerEx(0.5, barrel, 0, 0);         // prepare to kill the barrel in 0.6 second
      Return;
   }

   SetThingUserData(barrel, barrelhealth - damage);
   Return;

# ............................................................................................

timer:
   KillTimerEx(GetSenderId());
   CreateThing(barrel_exp, GetSenderId());   // create an explosion
   DestroyThing(GetSenderId());              // Destroy the barrel
   Return;
This is the same cog that we mentioned in the threads tutorial when we were talking about sleeps. If we explode the barrel immediately after it's damaged, then players won't have time to get away and we'd lose that element of suspense. Using a sleep here would be bad because we can easily have more than five barrels blow up at the same time - the thread limit would be exceeded and not all of the barrels would blow up. The solution is to use timers. We need to use IDed timers because more than one barrel can be waiting to explode. To get something unique for a timer's ID, we can use the barrel's thing number. And in the timer handler, we can retrieve the ID and use it to identify the barrel.

Notice that KillTimerEx() is used to stop a timer that has the same ID as the one that has expired. We're doing this as a safeguard in case two timers were set for the same barrel. We can't destroy something twice, but we shouldn't have to worry about the consequences. Also, thing numbers are not unique to one object in a game. Once an object is removed from gameplay, a thing number is free to be reused. So in some circumstances, you should check a thing's signature to make sure that your thing hasn't been destroyed and the number used for something else.

Using Pulses

A pulse works just like a timer set with SetTimer() - the difference is that a pulse 
automatically resets itself after its timer expires. If you need to create a continuous effect for an 
extended period of time, then it's a given that you'll need to run the same code every so many seconds. 
Pulses are more convenient for us to use because we don't have to reset them, and using a pulse frees 
up our timer messages for other things.

A pulse is set with SetPulse() or SetThingPulse(), and it's stopped with SetPulse(0) and SetThingPulse(0). SetPulse() works with just one cog like SetTimer(). SetThingPulse() works with a thing just like SetThingTimer().

Suppose, for example, that you want to create a rain effect in part of your level. What you need to do is create rain objects and make them fall towards the floor - as soon as they touch a surface, they can be removed. This is an effect that needs the same code to run in short intervals. For the sake of example:

#--------------------------------------------------------
startup:
	SetPulse(burstInterval);

Return;
#--------------------------------------------------------
pulse:
	for(i=0; i < numBurstElements; i=i+1) SetTimerEx(Rand(), Rand() * 6, 0, 0);

Return;
#--------------------------------------------------------
timer:
	if(GetSenderID() > 5) Return;	// just in case Rand() actually returns 1
	parent = ghost0[GetSenderID()];
	precip=FireProjectile(parent, precipTemp, -1, -1, RandVec(), '0 0 0', elementSpeedMult, 0x1, 0, 0);
	SetThingVel(precip, VectorSet(xMove, yMove, VectorZ(GetThingVel(precip))));

Return;
#--------------------------------------------------------
Both pulses and timers are used to create the rain effect. Timer doesn't have to be used, the pulse 
could be a little bit faster, but this rain implementation is using bursts with different intervals than 
the raindrops in the burst. This example shows that by using a pulse, you can use timers for other things.

Review


We'll that's the end of the DM's tutorials. Congrats and thankyou if you read through all of them. It's the 
author's intent with this last chapter to explain a few things that most tutorials won't try to explain 
because their way too specific.

Yeah, some of these tutorials are really easy, and calling this an "advanced" chapter is kind of an exaggeration, but you were warned at the beginning that the tutorials were not in any order or difficulty level.

Post a comment

Your comment will be anonymous unless you join the community. Or sign in with your social account: