3D Game Programming All in One (Course Technology PTR Game Development Series)

I'm not going to make you type in great reams of program code at this stage of the game, though you don't get off the hook completely. You will have to make some changes to accommodate the new stuff, and we'll also examine the contents of some of the new stuff to see what it does.

MenuScreen Interface Code

Open the file C:\koob\control\client\initialize.cs and locate the function InitializeClient and add the following lines in the grouping with the other similar statements:

Exec("./misc/ServerScreen.cs"); Exec("./misc/HostScreen.cs"); Exec("./misc/SoloScreen.cs"); Exec("./interfaces/ServerScreen.gui"); Exec("./interfaces/HostScreen.gui"); Exec("./interfaces/SoloScreen.gui");

Like I promised, I won't make you type in all the files referenced in those exec statements; you can copy them from C:\3DGPAi1\RESOURCES\CH23 and put them into the directories under the C:\koob\control\client\ directory in the subdirectories specified in the exec statements.

Each of these files is basically one module split into two parts. The actual interface definitions are in the files with the .gui extensions, while the code that manages the interfaces are in the files with the same prefix name, but ending with the .cs extension.

If you go back to the previous code listing for MenuScreen.gui, you will see where each of the interfaces is invoked. ServerScreen is defined in ServerScreen.gui, HostScreen is defined in HostScreen.gui, and finally, SoloScreen is defined in SoloScreen.gui.

Each interface has roughly the same form. There is an OnWake method for the interface object that is called by the engine when that object is displayed by the SetContent call in the related button in the MenuScreen interface. This method prepares the interface and fills the various data fields in the interfaces.

SoloPlay Interface Code

The SoloPlay interface that you saw in Figure 23.2 prepares a list of mission files that it finds so that you can select one of them to play with. The functional code for the SoloPlay interface, extracted from SoloPlay.cs, is shown here for discussion:

function PlaySolo() { %id = SoloMissionList.getSelectedId(); %mission = getField(SoloMissionList.getRowTextById(%id), 1); StopMusic(AudioIntroMusicProfile); createServer("SinglePlayer", %mission); %conn = new GameConnection(ServerConnection); RootGroup.add(ServerConnection); %conn.setConnectArgs("Reader"); %conn.connectLocal(); } function SoloScreen::onWake() { SoloMissionList.clear(); %i = 0; for(%file = findFirstFile($Server::MissionFileSpec); %file !$= ""; %file = findNextFile($Server::MissionFileSpec)) if (strStr(%file, "CVS/") == -1 && strStr(%file, "common/") == -1) SoloMissionList.addRow(%i++, getMissionDisplayName(%file) @ "\t" @ %file ); SoloMissionList.sort(0); SoloMissionList.setSelectedRow(0); SoloMissionList.scrollVisible(0); } function getMissionDisplayName( %missionFile ) { %file = new FileObject(); %MissionInfoObject = ""; if ( %file.openForRead( %missionFile ) ) { %inInfoBlock = false; while ( !%file.isEOF() ) { %line = %file.readLine(); %line = trim( %line ); if( %line $= "new ScriptObject(MissionInfo) {" ) %inInfoBlock = true; else if( %inInfoBlock && %line $= "};" ) { %inInfoBlock = false; %MissionInfoObject = %MissionInfoObject @ %line; break; } if( %inInfoBlock ) %MissionInfoObject = %MissionInfoObject @ %line @ " "; } %file.close(); } %MissionInfoObject = "%MissionInfoObject = " @ %MissionInfoObject; eval( %MissionInfoObject ); %file.delete(); if( %MissionInfoObject.name !$= "" ) return %MissionInfoObject.name; else return fileBase(%missionFile); }

The OnWake method is as described in earlier chapters—in this case the onWake method makes clear the mission list and then populates it according to the matching files it finds in the path indicated by $Server::MissionFileSpec. This variable is set in the file C:\koob\control\server\initialize.cs with the following line in the InitializeServer function:

$Server::MissionFileSpec = "*/maps/*.mis";

There are a couple of things you should understand about the way the search is done in the code presented.

First, there is the matter of the syntax used here. It can be difficult to decipher C-based code because of the looseness allowed—and Torque Script's syntax is extremely close to that of the C language and C++. You will recall that with most statements that employ a code block, such as if and for, you can use the long form or the short form, depending on your needs.

For example, the long form using braces

if (%a==1) { %x=5; }

can also be written as

if (%a==1) { %x=5; }

or as

if (%a==1) { %x=5; }

There are also other minor variations, but I'm sure you get the idea. The compiler doesn't care about the lines the code appears on, and it doesn't care about the amount of white space (tabs, spaces, and carriage returns). It only cares that the correct tokens and keywords are in the right place and that they make sense according to the compiler's parsing rules. Of course, white space is used to separate tokens and keywords, but the amount is not important to the parser.

The short form of these kinds of statements does depend on statement context, however. First, note that the preceding code can also be written as

if (%a==1)% x=5;

This demonstrates that the braces in the earlier example are superfluous in this particular flavor of statement. However,

if (%a==1) %x=5;

is a valid rendition of the short form—but the conditional code that you want executed must exist as a single statement that immediately follows the conditional test. In this example, if the test is satisfied, %x is assigned the value 5. If the test is not satisfied, the ensuing assignment is not carried out.

However, using the same form

if (%a==1) %x=5; %b=6;

if the test is satisfied, %x is assigned the value 5 as before, and %b is assigned the value 6. But (and this is a big but) if the test is not satisfied, although the ensuing assignment statement is not carried out, the one after it still is. So with this last bit of code, %b always gets assigned the value 6.

By now you may be wondering why this digression—here's why: The SoloScreen::onWake method has the following statements that search for available mission files to use to populate its list:

for(%file = findFirstFile($Server::MissionFileSpec); %file !$= ""; %file = findNextFile($Server::MissionFileSpec)) if (strStr(%file, "CVS/") == -1 && strStr(%file, "common/") == -1) SoloMissionList.addRow(%i++, getMissionDisplayName(%file) @ "\t" @ %file );

You might be tempted to misinterpret this code, even if you thoroughly understand programming in C or Torque Script. What we need to do is simplify the code to remove obfuscation introduced by the line context: We'll change all instances of findFirstFile($Server::MissionFileSpec) to fFF(), all instances of findNextFile ($Server::MissionFileSpec)) to fNF(), and finally, all instances of getMissionDisplayName (%file) to gMDN(). Now the code will look like this (it won't compile, but we don't care about that):

for(%file = fFF(); %file !$= ""; %file = fNF()) if (strStr(%file, "CVS/") == -1 && strStr(%file, "common/") == -1) SoloMissionList.addRow(%i++, gMDN()@ "\t" @ %file );

If we tidy up the white space a bit, we get this:

for(%file = fFF(); %file !$= ""; %file = fNF()) if (strStr(%file, "CVS/") == -1 && strStr(%file, "common/") == -1) SoloMissionList.addRow(%i++, gMDN()@ "\t" @ %file );

And hey, presto! The code structure reveals the algorithm quite nicely. The original line wrapping made the code hard to understand and made it look wrong when it actually wasn't. There are several lessons to be learned here:

  1. Make sure your programming editor lets you display long lines of maybe 150 characters or more, just in case you have them.

  2. Pay attention to your function and variable name lengths. Long descriptive names are extremely useful when you are trying to understand unfamiliar or long-forgotten code, but there are times when they can confuse more than explain.

  3. Your own code may confuse you at some later point just as much as it might confuse someone else who needs to understand it (someone you've called in to fix bugs for you, for example).

What fix do I recommend for this? Shorter names? No, use braces and indenting and put the statements in the long form in order to remove any contextual ambiguity.

for(%file = findFirstFile($Server::MissionFileSpec); %file !$= ""; %file = findNextFile($Server::MissionFileSpec)) { if (strStr(%file, "CVS/") == -1 && strStr(%file, "common/") == -1) { SoloMissionList.addRow(%i++, getMissionDisplayName(%file) @ "\t" @ %file ); } // end of if } // end of for

You can also add comments if they clarify what you are doing. Don't worry about insulting the intelligence of expert programmers by doing this. Any seasoned hand will greatly appreciate anything you do to make it quick and easy to understand what you are doing. Especially if they are doing code reviews for you!

Now, after that long-windedness, we can address the second issue about that code: What does it do?

The initial findFirstFile uses the variable to search the specified directory for the first instance of a matching file. If you actually do find a match, the path name is deposited in the %file variable, and you enter a loop. In each iteration of the loop, calls are made to findNextFile, which will find any new file in the sequence that matches the search criteria. If findNextFile does not find any more matching files, the %file variable is set to NULL, and the loop exits. In the loop we check the contents of the path name in %file for the existence of two potential invalid directory names: CVS (used for source code management, and not part of Torque) and common. If the file we found is not in either of those two directories, then we consider it to be valid and add it to our mission list using the SoloMissionList.addRow method.

The findFirstFile-findNextFile construct is a powerful one. It maintains an internal list of the files that it has found on your behalf. You just need to extract the path names as they appear.

Having said all that about such a small chunk of code, I should point out that this interface is a basic one. You might consider adding a few more capabilities, such as the sequence or random map selection option you'll find next in the Host interface.

The getMissionDisplayName is a bigger and more impressive-looking bit of work, but its function is fairly straightforward, albeit with a semimagical twist to it, so to speak. It opens up a file as directed and looks through it for the line that contains the statement "%MissionInfoObject =". It then creates an actual MissionInfoObject using that statement and uses the name property of the object to obtain the name and return the name to the calling function. This is a pretty clever way to examine a file. Pretty sensible too, when you realize that mission files are simply Torque Script files with a different extension.

This bit of code presents to you a lot of possibilities about how you can use Torque Script. One that comes to mind is a reprogrammable AI robot, where you merely read in the new instructions at run time, with the instructions written in Torque Script. No need to create your own robot control language!

Host Interface Code

The Host interface code is similar to the SoloPlay code that you've just looked at. There is nothing remarkable about it that hasn't been already mentioned, except that you should add some code to provide the player the ability to choose between playing maps in sequence (as exists now) or randomly.

You will want to have the Sequence and Random buttons that I've already provided in HostScreen.gui set a variable that your onWake code can examine. If the variable has one value, leave things as they are. If the variable has a different value, then have the onWake method choose a map randomly. One simple method to introduce the randomness is to select a random value between 0 and the number of available maps, and then to reject that many maps when the findNextFile function returns them. Then you would accept the next map returned.

Give it a try.

FindServer Interface Code

You saw the FindServer interface way back there in Figure 23.4. It lets you browse for servers with which you can connect. We've already looked at how this part of Torque works back in Chapter 5, 6, and 7, so I won't go into too much detail here. The functional code for the FindServer interface, extracted from ServerScreen.cs, is shown here for a brief discussion:

function ServerScreen::onWake() { MasterJoinServer.SetActive(MasterServerList.rowCount() > 0); } function ServerScreen::Query(%this) { QueryMasterServer( 0, // Query flags $Client::GameTypeQuery, // gameTypes $Client::MissionTypeQuery, // missionType 0, // minPlayers 100, // maxPlayers 0, // maxBots 2, // regionMask 0, // maxPing 100, // minCPU 0 // filterFlags ); } function ServerScreen::Cancel(%this) { CancelServerQuery(); } function ServerScreen::Join(%this) { CancelServerQuery(); %id = MasterServerList.GetSelectedId(); %index = getField(MasterServerList.GetRowTextById(%id),6); if (SetServerInfo(%index)) { %conn = new GameConnection(ServerConnection); %conn.SetConnectArgs($pref::Player::Name); %conn.SetJoinPassword($Client::Password); %conn.Connect($ServerInfo::Address); } } function ServerScreen::Close(%this) { cancelServerQuery(); Canvas.SetContent(MenuScreen); } function ServerScreen::Update(%this) { ServerQueryStatus.SetVisible(false); ServerServerList.Clear(); %sc = getServerCount(); for (%i = 0; %i < %sc; %i++) { setServerInfo(%i); ServerServerList.AddRow(%i, ($ServerInfo::Password? "Yes": "No") TAB $ServerInfo::Name TAB $ServerInfo::Ping TAB $ServerInfo::PlayerCount @ "/" @ $ServerInfo::MaxPlayers TAB $ServerInfo::Version TAB $ServerInfo::GameType TAB %i); // ServerInfo index stored also } ServerServerList.Sort(0); ServerServerList.SetSelectedRow(0); ServerServerList.scrollVisible(0); ServerJoinServer.SetActive(ServerServerList.rowCount() > 0); } function onServerQueryStatus(%status, %msg, %value) { if (!ServerQueryStatus.IsVisible()) ServerQueryStatus.SetVisible(true); switch$ (%status) { case "start": ServerJoinServer.SetActive(false); ServerQueryServer.SetActive(false); ServerStatusText.SetText(%msg); ServerStatusBar.SetValue(0); ServerServerList.Clear(); case "ping": ServerStatusText.SetText("Ping Servers"); ServerStatusBar.SetValue(%value); case "query": ServerStatusText.SetText("Query Servers"); ServerStatusBar.SetValue(%value); case "done": ServerQueryServer.SetActive(true); ServerQueryStatus.SetVisible(false); ServerScreen.update(); } }

Here the OnWake method makes the list active if there is anything already available from a previous incarnation to list. It's invoked as soon as the interface object is displayed on the screen.

When you click the Query Master button, the Query method is called, which sends a query packet to the master server, informing the master about what sort of servers are of interest. If the master server returns any information, it is deposited in the server information list, the Update method is invoked, and the list is created on the screen. This back-and-forth transaction is described in greater detail in Chapter 6.

The onServerQueryStatus method handles the various responses from the master server and deposits returned information, according to the changing states, into the various fields of the list.

ChatBox Interface Code

Open the file C:\koob\control\client\Initialize.cs and add the following lines to the function InitializeClient:

Exec("./misc/ChatBox.cs"); Exec("./misc/MessageBox.cs");

These exec statements load the new files that will provide our chat interface. You can copy them from C:\3DGPAi1\RESOURCES\CH23 and put them into the directories under the C:\koob\control\client\ directory in the subdirectories specified in the exec statements.

The ChatBox interface receives its text via a rather convoluted route. The message text originates at one of the clients and is sent to the server. The server receives the typed message and passes it to some common code that handles chat messages between the server and the client. Once the message arrives at the client common code, it is passed to the message handler called onChatMessage, which we provide in our client control code in our ChatBox.cs module. There is a parallel handler we are expected to supply in our client control code called onServerMessage, which is essentially the same as the one for the chat messages. These two functions look like this:

function onChatMessage(%message, %voice, %pitch) { if (GetWordCount(%message)) { ChatBox.AddLine(%message); } } function onServerMessage(%message) { if (GetWordCount(%message)) { ChatBox.AddLine(%message); } }

Not much needed here—just add the new text to the ChatBox object using its AddLine method.

The AddLine method is where all the heavy lifting is done; it looks like this:

function ChatBox::addLine(%this,%text) { %textHeight = %this.profile.fontSize; if (%textHeight <= 0) %textHeight = 12; %chatScrollHeight = getWord(%this.getGroup().getGroup().extent, 1); %chatPosition = getWord(%this.extent, 1) - %chatScrollHeight + getWord(%this.position, 1); %linesToScroll = mFloor((%chatPosition / %textHeight) + 0.5); if (%linesToScroll > 0) %origPosition = %this.position; while( !chatPageDown.isVisible() && MsgBoxMessageVector.getNumLines() && (MsgBoxMessageVector.getNumLines() >= $pref::frameMessageLogSize)) { %tag = MsgBoxMessageVector.getLineTag(0); if(%tag != 0) %tag.delete(); MsgBoxMessageVector.popFrontLine(); } MsgBoxMessageVector.pushBackLine(%text, $LastframeTarget); $LastframeTarget = 0; if (%linesToScroll > 0) { chatPageDown.setVisible(true); %this.position = %origPosition; } else chatPageDown.setVisible(false); }

We start out by getting the font size from the profile. We need this in order to determine the height and width spacing requirements for scrolling and frame sizing.

Then we use getGroup to obtain the handle for the object group this control belongs to. And we use that handle to get the parent group's handle. Then we use that handle to get the extent property that tells us the height and width of the parent object. We take the second value in the extent—which is the height—by using getWord to get word number 1, which is actually the second word. (We perverted programmers usually count starting at 0 instead of 1—but not always!)

The object retains the current output position using the position parameter, and that is used to calculate where the next position will be and saved as %chatPosition. We then use the calculations to figure out %linesToScroll, which dictates the text scroll action and the scroll bar actions.

Next, we enter a loop that extracts text from the text buffer called MsgBoxMessageVector line by line and inserts the lines in the ChatBox control.

Finally, we adjust the visibility of the scroll down prompt based on whether or not our position causes text to be out of sight at the bottom of the display.

MessageBox Interface Code

The MessageBox interface accepts our input from the keyboard.

We need to add a message handler to the server to receive the typed messages when they are sent from the client. Because of the context, it makes more sense to do that here than in Chapter 22, even though we are dealing with client issues in this chapter.

Open the file C:\koob\control\server\server.cs and add the following function to the end of the file:

function serverCmdTypedMessage(%client, %text) { if(strlen(%text) >= $Pref::Server::MaxChatLen) %text = getSubStr(%text, 0, $Pref::Server::MaxChatLen); ChatMessageAll(%client, '\c4%1: %2', %client.name, %text); }

This handler grabs the incoming typed message, makes sure that it isn't too long (we may want to restrict chat messages in order to preserve bandwidth requirements), and then sends the message to the common code server function called ChatMessageAll. The ChatMessageAll function will distribute the message to all of the other clients logged in to our game.

Next, let's look at the code that manages this on behalf of the MessageBox interface:

function MessageBox::Open(%this) { %offset = 6; if(%this.isVisible()) return; %windowPos = "8 " @ ( getWord( outerChatFrame.position, 1 ) + getWord( outerChat- Frame.extent, 1 ) + 1 ); %windowExt = getWord( OuterChatFrame.extent, 0 ) @ " " @ getWord( MessageBox_Frame.extent, 1 ); %textExtent = getWord(MessageBox_Text.extent, 0); %ctrlExtent = getWord(MessageBox_Frame.extent, 0); Canvas.pushDialog(%this); MessageBox_Frame.position = %windowPos; MessageBox_Frame.extent = %windowExt; MessageBox_Edit.position = setWord(MessageBox_Edit.position, 0, %textExtent + %off- set); MessageBox_Edit.extent = setWord(MessageBox_Edit.extent, 0, %ctrlExtent - %textExtent - (2 * %offset)); %this.setVisible(true); deactivateKeyboard(); MessageBox_Edit.makeFirstResponder(true); } function MessageBox::Close(%this) { if(!%this.isVisible()) return; Canvas.popDialog(%this); %this.setVisible(false); if ( $enableDirectInput ) activateKeyboard(); MessageBox_Edit.setValue(""); } function MessageBox::ToggleState(%this) { if(%this.isVisible()) %this.close(); else %this.open(); } function MessageBox_Edit::OnEscape(%this) { MessageBox.close(); } function MessageBox_Edit::Eval(%this) { %text = trim(%this.getValue()); if(%text !$= "") commandToServer('TypedMessage', %text); MessageBox.close(); } function ToggleMessageBox(%make) { if(%make) MessageBox.toggleState(); }

The Open method does some assignments of local variables based on the settings of properties of the MainChatBox object. This is so we can place the message box into a position relative to the chat display; in this case we are going to put it below and offset a little bit to the right.

Once we've done this, the code loads the MessageBox control into the Canvas using Canvas.pushDialog(%this), where %this is the handle of the MessageBox control object, and positions it according to the values of the earlier saved local variables.

When we've completed the positioning of the control, then the code makes it visible.

Next, the code turns off keyboard input for the Canvas object and sets the MessageBox_Edit subobject responsible for handling key inputs. From this point on, all typing goes into the MessageBox_Edit subobject, until something changes that.

The Close method removes the control from the Canvas, makes the control invisible again, and restores keyboard input handling to the Canvas.

The ToggleState method merely opens or closes the message box in a toggle fashion. If the control is open, it closes it, and vice versa.

The OnEscape method closes the control. This method is defined as the escapeCommand property value in the interface definition in MessageBox.gui.

The Eval method obtains the entered text, trims empty spaces from the end, and sends the text to the server as the parameter for a TypedMessage message, which the server knows how to handle.

Finally, the ToggleMessageBox method is bound to the "t" key in our presets.cs file. When it receives a non-null value in %make, it changes the current MessageBox open state using the ToggleState method.

Категории