Project Updates


Hour 7 Status

posted Jul 12, 2010, 9:17 PM by Unknown user

Coding is complete for Hour 7.  Hour 7 will add basic message passing to simulate a simple IRC client/server.  From there, we'll move on to command processing.  Should have the tutorial ready by tomorrow.

Hour 6 - Sending Data

posted Jul 11, 2010, 2:17 PM by Unknown user

Now that we have the client connecting to the server, we'll be able to send some data over the connection.

At this phase of development, we'll let the client send a packet by pressing the "T" key.  Normally you'd never want to do this -- even chat messages sent from the client must be throttled so as to prevent server flooding.  We will, however, be tightly controlling exactly what is sent.  As I mentioned previously, we want to be able to alter the data being sent - but only in very tight parameters, so to make the information interesting, we'll send the player location as two floats.

First, we're going to need precision control over keyboard input.  Open you client project and then add the following:

keyboardClient.dba

keyboardClientSetup:

   global keyboardKeyDown as boolean
   global keyboardNextKeyRepeat as float
   global keyboardRepeatDelay as float
   keyboardRepeatDelay = 500

   REM KEYBOARD INPUT MAP
   global KEYBOARD_KEY_SEND_TEST as integer
   KEYBOARD_KEY_SEND_TEST = 20

return

function keyboardClientPollTop()

   if keyboardKeyDown = 1
       if timer() > keyboardNextKeyRepeat
            keyboardKeyDown = 0
            keyboardNextKeyRepeat = timer() + keyboardRepeatDelay
       endif
   endif

   if scanCode() = 0
      keyboardKeyDown = 0
   endif

endfunction

function keyboardClientPollBottom()

   if keyboardKeyDown = 0

      if scanCode() <> 0
         keyboardKeyDown = 1
         keyboardNextKeyRepeat = timer() + keyboardRepeatDelay
      endif

      if keyState(KEYBOARD_KEY_SEND_TEST) = 1
         mediatorClient(MEDIATOR_KEYBOARD, MEDIATOR_NETWORK, KEYBOARD_KEY_SEND_TEST)
      endif

   endif

endfunction

These two modules allow us to accept keyboard input, but only at the rate specified by keyboardRepeatDelay.  This is milliseconds, of course, so this is 2 key inputs per second.  This will prevent flooding not only for typing, but also for avatar control input.

Now create a NEW module called "mediatorClient.dba" and add the following code:

mediatorClient.dba

mediatorClientSetup:
   #constant MEDIATOR_KEYBOARD      1
   #constant MEDIATOR_NETWORK       2
return

function mediatorClient(fromClass as integer,toClass as integer,msgID as integer)

   select fromClass
      REM KEYBOARD MEDIATOR USES KEYBOARD INPUT
      REM INPUT MAPPINGS AS THE MESSAGE ID
      case MEDIATOR_KEYBOARD
         select toClass
            case MEDIATOR_NETWORK
               if msgID = KEYBOARD_KEY_SEND_TEST
                  netClientSendTestMessage()
               endif
            endcase
         endselect
      endcase

      REM ADD ADDITIONAL MEDIATOR CONTROLLERS HERE

   endselect

endfunction

Seems really strange, doesn't it?  The idea here is that we don't want the keyboard to have to know anything about the network, and the network shouldn't have to know anything about the keyboard.  So, we create this mediator "class" that knows how to talk to both through well-defined interfaces.  This greatly enhances code portability, as the original modules (in this case the networkClient and keyboardClient modules) can be re-used with other projects as-is and only the mediator needs to change.  I realize that's probably difficult to see with all the hard-coding we have going on right now, but it'll be clearer once the hard-coded values turn user-definable options.

OK, we're still in the client project, so let's update the netClient and netShared while we're here:

Add the following function to netClient.dba:

netClient.dba (partial)

function netClientSendTestMessage()

   REM FOR THE TEST MESSAGE, WE WILL SEND
   REM THE CLIENT LOCATION AS TWO FLOATS
   netSharedAddHeader(NETC_TEST_MESSAGE,20)
   x# = object position x(5)
   z# = object position z(5)
   mn Add Float SendPacket, x#
   mn Add Float SendPacket, z#
   mn Send TCP 0,SendPacket,0,0,1
   debugWrite(DEBUGINFO,"Sent NETC_TEST_MESSAGE with data " + str$(x#) + "," + str$(z#))

endfunction

...now add this function to netShared.dba:

netShared.dba (partial)

function netSharedAddHeader(msgID as integer,size as integer)

    REM UTILITY FUNCTION TO INSURE
    REM HEADER IS ALWAYS CORRECT
    REM LENGTH IS UPDATED ON SEND
    REM MINIMUM SIZE IS 12 BYTES
    mn Clear Packet SendPacket
    mn Add Int SendPacket, size
    mn Add Int SendPacket, timer()
    mn Add Int SendPacket, msgID

endfunction

Our "header" for every message is going to be well-defined so that we can double-check what the size of the message should be against the actual size.  If these don't match, then it's a sure sign that you have a corrupt message and the receiver can safely ignore it.  Also, the time sent (according to the sender) will be important to detect out-of-order messages, which again will be dropped -- this will become critically important later when we get to movement prediction.  And of course, every message needs a message ID so that the receiver knows what to do with the rest of the data in the packet.

Alright, now we need to link these all together

Update your worlofomen.dba to this:

worldofomen.dba

sync on : sync rate 0

gosub systemSharedSetup
gosub debugSharedSetup
gosub debugClientSetup
gosub netSharedSetup
gosub netClientSetup
gosub keyboardClientSetup


REM MEDIATOR SHOULD ALWAYS
REM BE SETUP LAST
gosub mediatorClientSetup

REM STARTUP FUNCTIONS HERE
netClientStart()
netClientConnectStart()


gosub cameraClientSetup

gosub skyboxClientSetup
skyboxLoad(2)

gosub terrainClientSetup
terrainLoad(2,2)


REM Our avatar
make object cylinder 5,2


do

   keyboardClientPollTop()
   netClientMaintain()

   control camera using arrowkeys 0,1,1
   position object 5, camera position x(0), camera position y(0), camera position z(0)
   rotate object 5, 0, camera angle y(0), 0
   move object 5, 4
   sync

   keyboardClientPollBottom()

loop

Go ahead and SAVE ALL and COMPILE.  Alright, let's put the client aside for now - close that project and open your server project.  All we have to do on the server side is add a function to parse TCP messages.  We've already call the call for this in place, and it just needs to be uncommented.

Open your server project and open netServer.dba.  In there, search for the text "netServerParseTCP(instance, client)" and uncomment that line, then add the function to the end:

netServer.dba

function netServerParseTCP(instance, client)

   local result as integer
   result = 0
   local temp$ as string
   local temp as integer

   if client < 1 or client > NET_CLIENTS_PER_INSTANCE then exitfunction

   msgDeclaredLen = mn Get Int(RecvPacket)
   msgTstmp = mn Get Int(RecvPacket)
   msgID = mn Get Int(RecvPacket)
   msgActualLen = mn Get Used Size(RecvPacket)

   REM HERE WE NEED TO DO SOME SANITY CHECKS, WE'LL COME
   REM BACK TO THESE SOON ENOUGH

   select msgID

     case NETC_TEST_MESSAGE
         x# = mn Get Float(RecvPacket)
         z# = mn Get Float(RecvPacket)
         debugWrite(DEBUGINFO,"Message " + str$(msgID) + " recevied from client " + str$(client) + ": loc: " + str$(x#)+","+str$(z#))
     endcase

   endselect

endfunction

The network shared functions have already been updated, so that's all we need to do on the server.  Go ahead and SAVE ALL and COMPILE and RUN.  While the server is running, go ahead and start up the client as well.  Move around in the client and hit the "T" key a few times and you'll see the server receiving you avatar location.

If you've done everything right, you should have something that looks like this:



Next hour, we'll be extending this functionality slightly to allow any arbitrary text (well, within limits) to be sent back and forth between the client and server and that will be a basic chat client.

Thanks for reading!



Hour 5 - Network Framework

posted Jul 9, 2010, 9:37 PM by Unknown user   [ updated Feb 25, 2011, 12:24 PM ]

Edit:  This page has been updated to allow the code base to support DarkNet 2.0 -- these modifications are highlighted in the code with bright green text.

This hour, we're going to add a couple of features to the client and the server:
  • Starting the network subsystem
  • Putting the server in listen mode
  • Making a connection from the client to the server
  • Detecting the connecting on the server
In the spirit of keeping each of these tutorial hours as short and as clear as possible, that's all we'll tackle in this hour.  

First, let's create our dataObject.  Network sessions, just like everything else, will eventually use this type.  Open your server project, and add the following:

dataShared.dba

dataSharedSetup:

   type dataObjectType
      key as string
   endtype

return

Not much to say about this (yet).  I'll have lots more to add on later, though.    Now, since we're going to start using debugging on the client, we'll need to tell the client exactly what we want done with these debugging message:

debugClient.dba

debugClientSetup:

   debugMode = 0

return

OK, now we're going to need a few common elements between the client and server so that they can speak the same language to each other.

netShared.dba

netSharedSetup:

   #constant NET_SERVER_NAME_DEFAULT "127.0.0.1"
   global NET_SERVER_NAME as string

   global NET_SERVER_PORT as integer
   NET_SERVER_PORT = 9669

   global NET_INSTANCES as integer
   NET_INSTANCES = 1

   global NET_THREADS as integer
   NET_THREADS = 0
   REM SETTING THREADS TO ZERO WILL CAUSE SERVER
   REM TO START 1 THREAD PER PHYSICAL CPU

   global NET_UDP_MODE as integer
   NET_UDP_MODE = 2

   global NET_TIMEOUT_SECONDS as integer
   NET_TIMEOUT_SECONDS = 10

   global NET_UDP_UPDATE_INTERVAL as integer
   NET_UDP_UPDATE_INTERVAL = 250

   global NET_UDP_HEARTBEAT_INTERVAL as integer
   NET_UDP_HEARTBEAT_INTERVAL = 5000

   global NET_TCP_RECONNECT_INTERVAL as integer
   NET_TCP_RECONNECT_INTERVAL = 5000 + rnd(2000)

   global SendPacket as dword
   global RecvPacket as dword

   SendPacket = mn create packet()
   RecvPacket = mn create packet()
   mn set memory size SendPacket,1024
   rem RecVPacket size is set automatically on receive


   REM CLIENT TCP OPERATIONS
   #constant NETC_TEST_MESSAGE            1

   REM DARKNET 2.0
   global NET_PROFILE_STANDARD as dword
 

return

Now we'll add the network framework to the server - just the setup, start, stop, and maintain functions.

netServer.dba

netServerSetup:


   REM ALL OF THE HARD-CODED VALUES YOU SEE HERE
   REM WILL BE REPLACED BY SERVER NETWORK OPTIONS
   REM ONCE WE HAVE THE DATA SYSTEM MODULES
   REM IN PLACE - HARD CODING ALL THESE
   REM VALUES FOR NOW TO KEEP THINGS AS CLEAR
   REM AS POSSIBLE

   global netServerPlayerCount as integer

   global NET_CLIENTS_PER_INSTANCE as integer
   NET_CLIENTS_PER_INSTANCE = 50

   global dim netSession(NET_CLIENTS_PER_INSTANCE) as dataObjectType

   REM NET_MAX_OPERATIONS IS THE TOTAL DIFFERENT KINDS OF UDP
   REM MESSAGES TO PROCESS.  SINCE WE'RE ONLY DOING
   REM MOVEMENT AND JUMPING VIA UDP FOR NOW, 2 IS ENOUGH
   global NET_MAX_OPERATIONS as integer
   NET_MAX_OPERATIONS = 2

   REM WE WILL PUT IN A SYSTEM LATER TO DYNAMICALLY
   REM GET THIS VALUE OR GET IT FROM AN ASSIGNED
   REM OPTION, BUT AGAIN, HARD CODING FOR CLARITY
   NET_SERVER_NAME = "192.168.1.4"

   REM TURN OF DARKNET DEBUG ON SERVER SO WE DON'T
   REM SEE POPUP WINDOWS FOR WARNINGS
   mn Toggle Error Mode 1


return

function netServerCreateProfile()

   REM DARKNET 2.0
   myProfile = mn create instance profile()
   mn Set Profile Local myProfile, NET_SERVER_NAME, NET_SERVER_PORT, NET_SERVER_NAME, NET_SERVER_PORT
   mn Set Profile Mode UDP myProfile, NET_UDP_MODE
   mn Set Profile Num Operations UDP myProfile, NET_MAX_OPERATIONS

endfunction myProfile

function netServerStart()


   mn start NET_INSTANCES, NET_THREADS
   NET_PROFILE_STANDARD = netServerCreateProfile()
   for i = 0 to (NET_INSTANCES - 1 )
   
      retval = mn Start Server(i, NET_CLIENTS_PER_INSTANCE, NET_PROFILE_STANDARD)
      if retval = 0
         debugWrite(DEBUGINFO,NET_SERVER_NAME + " listening on port " + str$(NET_SERVER_PORT) + " as instance " + str$(i))
         debugWrite(DEBUGINFO,"Clients/Instance set to " + str$(NET_CLIENTS_PER_INSTANCE))
         debugWrite(DEBUGINFO,"UDP Max Operations set to " + str$(NET_MAX_OPERATIONS))
         debugWrite(DEBUGINFO,"UDP Mode set to " + str$(NET_UDP_MODE))
         debugWrite(DEBUGINFO,"Press spacebar to quit.")
      else
         debugWrite(DEBUGERROR,"Server network start error " + str$(retval))
         `systemGameModeAdd(SYSTEM_MODE_QUITTING)
         rem not yet
      endif
   next i

endfunction

function netServerStop()

   mn Finish -1

endfunction

function netServerSessionReset(client as integer)

   if client < 1 or client > NET_CLIENTS_PER_INSTANCE
      debugWrite(DEBUGWARN,"Attempted to reset session out of range " + str$(client))
      exitfunction
   endif

   REM SETTING THE SESSION KEY TO THE INVALID VALUE
   REM GUARANTEES THAT NO MORE DATA CAN BE RECEIVED
   REM ON THIS CIRCUIT UNTIL IT IS REBUILT
   netSession(client).key=""

endfunction


function netServerMaintain()

   local instance as integer
   local client as integer
   local joinID as integer
   local leftID as integer
   local temp$ as string
   local temp as integer
   local TCPPackets as integer
   local UDPPackets as integer
   local operationID as integer

   for instance = 0 to (NET_INSTANCES - 1 )


      REM TAKE CARE OF CLIENT JOINS

      joinID = mn Client Joined(instance)
      if joinID > 0

         temp$ = mn Get Client IP TCP(instance, joinID)
         temp = mn Get Client Port TCP(instance, joinID)

         REM INITIAL CONNECTS ARE "INFORMATIONLESS" AND SO WE
         REM DO NOT YET HAVE ANY INFORMATION TO SHARE WITH OTHERS
         REM UNTIL LOGON IS COMPLETED - AT WHICH POINT, A PLAYER
         REM JOIN ANNOUNCEMENT WILL BE SENT

         debugWrite(DEBUGINFO,"Connection detected from " + temp$ + " on port " + str$(temp))

         netServerSessionReset(joinID)

      endif

      REM TAKE CARE OF CLIENT DISCONNECTS

     leftID = mn Client Left(instance)
     if leftID > 0

        if leftID <= NET_CLIENTS_PER_INSTANCE
           `netServerDropObject(instance, leftID, leftID)
           `netServerChatBroadcast(instance, "SYSTEM", dataObject(leftID).avatarname + " has left.", leftID)
           rem not yet
        endif

      netServerSessionReset(leftID)

     endif

     REM TAKE CARE OF UDP AND TCP MESSAGES
     for client = 1 to NET_CLIENTS_PER_INSTANCE
         TCPPackets = mn Recv TCP(instance,RecvPacket,client)
         if TCPPackets > 0
            `netServerParseTCP(instance, client)
            rem not yet
         endif
         for operationID = 0 to (NET_MAX_OPERATIONS - 1)
            UDPPackets = mn Recv UDP(instance,RecvPacket,client,operationID)
            if UDPPackets > 0
               `netServerParseUDP(instance, client, operationID)
               rem not yet
            endif
         next operationID
     next client
   next instance

endfunction



The network module on the client is going to be much simpler to start, stop and maintain (for now).  Later, we'll be adding connectivity testing, and the ability for the client to recover gracefully from dropped connections. 

netClient.dba

netClientSetup:

   global netClientConnected as boolean
   NET_SERVER_NAME = "192.168.1.4"

return

function netClientCreateProfile()

   REM DARKNET 2.0
   myProfile = mn create instance profile()

endfunction myProfile


function netClientStart()

   mn start 1, 0
   mn Toggle Error Mode 1
   NET_PROFILE_STANDARD = netClientCreateProfile()

endfunction

function netClientStop()

   mn Finish -1

endfunction

function netClientConnectStart()

   debugWrite(DEBUGINFO,"CONNECT START: " + NET_SERVER_NAME + ":" + str$(NET_SERVER_PORT) + ", Profile: " + str$(NET_PROFILE_STANDARD))
   myConnect = mn Connect(0,NET_SERVER_NAME,NET_SERVER_PORT,NET_SERVER_NAME,NET_SERVER_PORT,(NET_TIMEOUT_SECONDS * 1000),0,NET_PROFILE_STANDARD)

endfunction

function netClientConnectStop()

   mn Stop Connect 0
   if (systemMode && SYSTEM_MODE_NET_TCP)
      debugWrite(DEBUGERROR,"Net Connect timeout or error connecting to server.")
   endif
   `systemGameModeRemove(SYSTEM_MODE_NET_TCP)
   rem not yet

   msg$ = "Lost connection to server... attempting to reconnect.  Press Cancel to stop."

endfunction

function netClientMaintain()

   if netClientConnected = 0
      pollStatus = mn poll connect(0)
      if pollStatus = 1
         REM SUCCESSFUL
         `systemGameModeAdd(SYSTEM_MODE_NET_TCP)
         rem not yet
         debugWrite(DEBUGINFO,"Net Connect Successful.  Mode TCP Added.")
         netMaxUDPClients = mn Get Max Clients(0)
         debugWrite(DEBUGINFO,"Net Max UDP Clients set to " + str$(netMaxUDPClients))
         netMaxUDPOperations = mn Get Max Operations(0)
         debugWrite(DEBUGINFO,"Net Max UDP Operations set to " + str$(netMaxUDPOperations))
         REM DARKNET 2.0
         netClientConnected = 1
      endif
   endif

endfunction



Now all we have left to do is to tie everything together in the main modules.

server.dba

REM SYSTEM SETUP
gosub systemSharedSetup
gosub debugSharedSetup
gosub debugServerSetup
gosub dataSharedSetup
gosub dataServerSetup
gosub netSharedSetup
gosub netServerSetup


REM SYSTEM START

netServerStart()

repeat

   netServerMaintain()

until (spacekey() > 0)


REM SYSTEM STOP

netServerStop()
debugStop()


In the client module, add the following before the call to the cameraClientSetup:

gosub systemSharedSetup
gosub debugSharedSetup
gosub debugClientSetup
gosub netSharedSetup
gosub netClientSetup

netClientStart()
netClientConnectStart()


Now start up the server, and then startup the client.  The server should now detect the client connection:


Next hour, we'll start sending message back and forth between the client and server - which means we'll finally be able to check off our first Milestone :o)

Thanks for reading!


P.S.  Thank you Slyvnr for pointing out that the IP Address settings in the client and server are hard-coded at this hour and will need to be updated to your local LAN IP address until you get to hour 12.

Here's the easiest way of taking care of this:

Start --> Run --> CMD
ipconfig /all

Find your LAN IP Address (should be something like 10.0.0.1 or 192.168.1.1 but last 3 or 2 number are likely to be different)

Put this address in the (checks version 5 of the repo) - aha:

...on line 10 in netclient.dba and on line 33 in netserver.dba like this (replace this with your IP address):

NET_SERVER_NAME = "192.168.1.4"

Unfortunately, until you get to hour 12, you'll need to keep updating this after every checkout - but you only need to update it once every checkout. 


Hour 4 - ID Management

posted Jul 6, 2010, 6:56 PM by Unknown user

Everything in your world is going to need its own unique number to identify it from... well, everything else in your world.  

But you probably already knew that.

The simplest solution to this problem is to just place all the objects in your world into a single array -- maybe some large "object" array that may contain pointers to sub-arrays for additional attributes specific to the object type.  This is the solution I see a lot, and it seems to be quite popular for a lot of Indy game developers.  Such a solution may look something like this:

 arrayid objecttype name
 1 player Mary
 2 player  Joe
 3 usable apple
 4 npc Lilith

In this case, the array element index ("arrayid") actually IS your globally-unique ID ("guid") -- or, at least that's what it's trying to do.  Unfortunately, there are a LOT of problems with doing things this way - here's just a few:

  1. If somehow even a single record is lost (say from not being saved properly), or somehow gets put out of order (say from not being read properly) then everything in your world will get shifted around.  People will look in their backpack one day to find their spellbook, and the next day they may look in and instead find the door to the local pub (which by the way will still work just fine - but having someone cart around the most popular pub in their backpack probably won't be something you'll want).

  2. Your array will have problems with "sparseness."  Let's say you have 100,000 accounts.  Let's say Bob just so happens to be Player 100,000.  Now every time Bob logs on to the game, the poor server has to allocate a 100,000 element jump table.  It doesn't matter if the only other people logged in are Joe (Player 1) and Mary (Player 2) - when Bob logs on, suddenly that array has to become 100,000 elements to accommodate him.  And it has to be re-dimensioned on the clients as well (Dang that Bob!)

  3. Absolutely every maintenance system in your game is going to have to search through the entire array to perform maintenance on the elements it cares about.  Let's say you have an area with 400 scene elements, but only one of these is a clock.  Let's say that every minute, you want to update the hands on the clock so that it shows the realworld time.  In reality, you'd never want to actually do something like this, but let's move on.  The problem is that the function that updates the "clock things" in the world will have to search through all 400 items to see if any are clocks that need updating.  Plus you'll have other functions that will need to update "car things" and still others to update "guard things" and so on.  In the end, you'll be spending so much time searching that the whole thing will grind to a halt.
So, this first solution is obviously not going to work.  The next most logical solution would be to go the DBA route - bring on the third normal form!  In this scenario, your data may look something like this:

People Array
 arrayidguid  name
 1 245 Mary
 2 426753 Joe
 3 8489 Lilith

Usable Objects Array
 arrayidguid name 
 112749 apple 

Inventory Array
 arrayid guid_person guid_object
 1 426753 12749

Now this may look great initially -- you may find yourself jumping up and down saying things like, "look, we can find out what's in someone's backpack just by doing a JOIN" or things like, "our array is only exactly as big as it needs to be!"  This will last a good month of so, but then an inevitable series of events will unfold:
  • You'll find that every time you want to add a field to your database, there will be a lot of changes needed in the code (yes, even if your language has introspection).  At the worst, you'll get to a point where you begin saying really terrible, awful things like, "features that require changes to the database are not allowed."

  • The relations between the various tables will become so complex that you really will need a full-time DBA on staff just to keep the whole thing sorted.  No one will know what they can do and what they can't do because only your DBA will understand it and she may not be the one you want to rely on for making game design decisions.  At this point you may find yourself saying things like, "Mary says we can't link together inventory to mission goals because then we'd have to built a whole new lookup table with cardinality equal to the sum of both tables, which wouldn't be pro-formant."

  • Eventually, you'll find that you are effectively coding a database engine instead of a game engine.  This will happen because the database won't do exactly what you want it to do (because what you want to do violates 3rd normal form of course).  Or, worse yet, you've been writing your own database engine from day 1 and you didn't realize it until you found yourself in an alley behind a jazz bar in Seattle, Googling over the complimentary WiFi for "index optimization techniques."  It's then that you'll know just how wrong you've been!
Now, I know there may be those among you right now saying things like, "but all those problems can be solved with code," or "Seattle is a great city!"  Well, with the second statement, I'd have to agree wholeheartedly - but for the first, not so much.  The problem you get into with "use code to fix database limitations," is that you end up (invariably) writing your own database engine.  Then, of course, the new guy you hire will find all kinds of problems with your hand-coded database engine and spend months converting everything back over to a Real Database, at which point the whole cycle starts all over.

Alright, so neither of these are going to work for us - so what is the solution?

These are really two far ends of a spectrum -- there are actually plenty of solutions in-between.  What's important to learn here is that going too far toward either "flexibility" or "correctness" is going to cause a lot of problems

So, our particular "middle ground" solution goes something like this:

We're going to use a globally unique ID for everything, but that GUID is going to be a "composite" number made up of a "local ID" and a "domain ID".  The local ID will be small and well-suited for being an array element index, which will allow direct access.  The "domain ID" will tell us what "type of thing" the object is.  The "local ID" and the "domain ID" are derived from the global ID through use of a bitmask.  So for example, we may use the leftmost 8 bits of our number (shifted right 24 bits) as the domain ID, and the rightmost 24 bits (not shifted) as the "local ID".  We're actually going to use 22 instead of the full 24, but I'll explain that later :)

In other words: 
( (guid && 4278190080) >> 24 ) = domainID
(guid && 16777215) = localID

and also:
guid = (domainID << 24) + localID

We're also going to be using multiple array for each type of object that will either: (a) use the local ID as the array index for cases where sparseness is low OR (b) use an arbitrary array index (auto-increment) and store the local ID as a separate attribute in cases where sparseness is high.  

The logic here is that if sparseness is low, then you might as well just use the localID as the array index.  This will be useful for things like scenery objects for an area where you'll normally want the array index to be dimensioned to the total count of objects for an area upon load so that it isn't re-dimensioned over and over as a player moves through an area.  On the other hand, if sparseness is high (like for player IDs), then you'll want to be able to arbitrarily add people in and out of the array as people logon and logoff.  In other words, all array will be turned into arrays with low sparseness by design.

Not only will this solve the problem of sparseness, it will also solve the search problem, AND the routine maintenance problem.  Searches will always be performed on the smallest possible array elements that it could possibly be performed upon (effectively pre-filtered by object type with low empty record counts).  Functions that need to perform maintenance on specific object types will find that most of the time the next most record they examine will be the one that needs to be checked for maintenance.

In addition, we're also going to be using the SAME object type for all objects.  In other words (in DBPRo parlance), we'll use the same UDT for all arrays.  This provides a couple of additional (and significant) benefits basically for free:

  • It will allow us to convert 1 object type to another object type simply by moving an element DIRECTLY from 1 array to another array.  We won't need to worry about updating the GUID because the domain ID is implicit from the array.  Only the local ID is stored in each array and the program will know which domainID is assigned to each array.  

  • We don't have to worry about duplication errors because the transaction from one object type to another object type (say for example for "lootable item" to "item in backpack") is intrinsically ATOMIC.  Once the record is is moved to a different array, the GUID of the item is changed automatically (due to being in a new array that has a different domainID) even in cases where the localID stays the same (unlikely but possible).  One object ceases to exist and an another object is created all in 1 step.

  • We can do interesting things, like making scenery come alive or making all players turn into wolves every full moon :)
Now that we have our solution, let's get into the implementation...

Tonight, we're going to implement the ID Management System (it's actually quite simple as it should be).

Remember, we're still working toward that goal of "Create basic network chat client/server", but we need to be able to grant unique IDs to our sessions and we might as well do it right -- so, this is more foundation work.  Now, hopefully, the publisher is still impressed and happy with the Land and Sky demo that she is OK with giving you a little more time on this first milestone.  Explain that you are doing foundational ID management work, and you should get a knowing nod, and a "make it so."  

Under your server folder, create a "data" folder and under there create the following folders: "account", "avatar", and "id".  Yep, those are arrays.

OK, now we need some supporting functions - namely some system functions and some debug functions.

Open your server project and add all of the following:

systemShared.dba

systemSharedSetup:

    global systemNextFileID as integer

    REM RESERVED FILE ID MANAGEMENT
    global SYSTEM_FILE_DATA_SERVERID as integer
    SYSTEM_FILE_DATA_SERVERID = systemGetFreeFileID()
    global SYSTEM_FILE_DATA_GUID as integer
    SYSTEM_FILE_DATA_GUID = systemGetFreeFileID()

return

function systemGetFreeFileID()
   inc systemNextFileID
endfunction systemNextFileID

function systemGetRealDateTime(dsep$,dtsep$,tsep$)

   date$ = get date$()
   time$ = get time$()

   month$ = left$(date$,2)
   day$ = mid$(date$,4) + mid$(date$,5)
   year$ = "20" + right$(date$,2)
   date$ = year$+dsep$+month$+dsep$+day$

   hour$ = left$(time$,2)
   minutes$ = mid$(time$,4) + mid$(time$,5)
   seconds$ = right$(time$,2)
   time$ = hour$+tsep$+minutes$+tsep$+seconds$

   datetime$=date$+dtsep$+time$

endfunction datetime$


debugShared:

debugSharedSetup:

   global debugMode as boolean
   REM 0=DO NOT PRINT TO SCREEN, 1=PRINT TO SCREEN

   `debugfilename$ = remove$(appName$(),".exe") + systemGetRealDateTime("_","_","_") + ".log"
   debugfilename$ = remove$(appName$(),".exe") + ".log"
   if file exist(debugfilename$)
      delete file debugfilename$
   endif

   OPEN LOG debugfilename$, 1
   SET LOG TAG 0, "[SYSTEM] INFO  ", ""
   SET LOG TAG 1, "[SYSTEM] WARN  ", ""
   SET LOG TAG 2, "[SYSTEM] ERROR ", ""
   SET LOG TAG 3, "[SYSTEM] HACK  ", ""
   SET LOG TAG 4, "[SYSTEM] CHAT  ", ""
   #constant DEBUGINFO 0
   #constant DEBUGWARN 1
   #constant DEBUGERROR 2
   #constant DEBUGHACK 3
   #constant DEBUGCHAT 4

return

function debugShutdown()

   CLOSE LOG

endfunction

function debugWrite(level as integer, msg$ as string)
   tstamp$ = systemGetRealDateTime("/"," ",":")
   WRITELN LOG level, "%s :: %s" , tstamp$, msg$
   if debugMode = 1 then print tstamp$, " :: ", msg$
endfunction

debugServer:


debugServerSetup:

   debugMode = 1

return


OK, now that we have all of that in place, now we'll be able to put in the ID Management functions.  Of course, because our server is authoritative, it will be the only one to choose GUIDs for objects - so all of this goes into the server:

dataServer:



dataServerSetup:


#constant DATA_PATH "server/data/"
#constant DATA_ID_PATH "server/data/id/"
#constant DATA_ACCOUNT_PATH "server/data/account/"
#constant DATA_AVATAR_PATH "server/data/avatar/"


REM CALCULATE OUR CRITICAL FILE PATHS
global dataguidfilename as string
global dataserveridfilename as string
dataguidfilename = DATA_ID_PATH + "guid.txt"
dataserveridfilename = DATA_ID_PATH + "serverid.txt"


REM DOMAIN 0: Client Temp IDs
REM DOMAIN 1 through 127: Game Server Instance Temp IDs
REM DOMAIN 128: Master Player Account IDs
REM DOMAIN 129 - 255: Game/Architecture specific domains


REM PARTITION OUR BITS
#constant DATA_GUID_BITS_TOTAL 30
#constant DATA_GUID_BITS_SERVERS 8
#constant DATA_GUID_BITS_OBJECTS 22


global dataServerID as integer
global dataNextGUID as integer
global dataMaxGUID as integer
global dataNextRPGID as integer




dataServerID = 1
dataMaxGUID = (2^DATA_GUID_BITS_OBJECTS)-1
dataNextGUID = 1


REM GET OUR SERVER ID
dataServerID = dataServerLoadServerID()
debugWrite(DEBUGINFO,"SERVER ID FOUND : " + str$(dataServerID))


REM GET OUR REAL NEXT GUID
dataNextGUID = dataServerLoadNextGUID()
debugWrite(DEBUGINFO, "NEXT GUID FOUND : " + str$(dataNextGUID))


return


function dataServerLoadServerID()
local retval as integer
retval = dataServerID


if file exist(dataserveridfilename) = 0
debugWrite(DEBUGWARN, "NO SERVER ID FILE FOUND, CREATING NEW")
open to write SYSTEM_FILE_DATA_SERVERID, dataserveridfilename
write string SYSTEM_FILE_DATA_SERVERID, str$(dataServerID)
close file SYSTEM_FILE_DATA_SERVERID
else
open to read SYSTEM_FILE_DATA_SERVERID, dataserveridfilename
read string SYSTEM_FILE_DATA_SERVERID, dat$
retval = intval(dat$)
close file SYSTEM_FILE_DATA_SERVERID
endif


endfunction retval


function dataServerLoadNextGUID()
local retval as integer
retval = dataNextGUID


if file exist(dataguidfilename) = 0
debugWrite(DEBUGWARN, "NO GUID FILE FOUND, CREATING NEW")
open to write SYSTEM_FILE_DATA_GUID, dataguidfilename
write string SYSTEM_FILE_DATA_GUID, str$(dataNextGUID)
close file SYSTEM_FILE_DATA_GUID
else
open to read SYSTEM_FILE_DATA_GUID, dataguidfilename
read string SYSTEM_FILE_DATA_GUID, dat$
retval = intval(dat$)
close file SYSTEM_FILE_DATA_GUID
endif


if retval > dataMaxGUID
debugWrite(DEBUGERROR, "MAX GUID REACHED, INCREASE DATA_GUID_BITS_OBJECTS, STOPPING")
`systemGameModeAdd(SYSTEM_MODE_QUITTING)
endif


endfunction retval


function dataServerSaveNextGUID()


if file exist(dataguidfilename) = 1
delete file dataguidfilename
open to write SYSTEM_FILE_DATA_GUID, dataguidfilename
write string SYSTEM_FILE_DATA_GUID, str$(dataNextGUID)
close file SYSTEM_FILE_DATA_GUID
else
debugWrite(DEBUGERROR, "GUID FILE DELETED AFTER STARTUP, STOPPING")
`systemGameModeAdd(SYSTEM_MODE_QUITTING)
endif


endfunction


function dataServerGetNextRPGID()


inc dataNextGUID
dataServerSaveNextGUID()
dataNextRPGID = (dataServerID << DATA_GUID_BITS_OBJECTS) + dataNextGUID


endfunction dataNextRPGID


Primarily what this is going to give us is our first domain -- which is the differentiation of one server instance from another - but of course, there are going to be a LOT more domains:

REM DOMAIN 0: Client Temp IDs <-- client local domain
REM DOMAIN 1 through 127: Game Server Instance Temp IDs <-- server domains
REM DOMAIN 128: Master Player Account IDs <-- player accounts
REM DOMAIN 129 - 255: Game/Architecture specific domains <-- arrays

OK, now we need to pull it all together.  

server.dba

sync on : sync rate 30

gosub systemSharedSetup
gosub debugSharedSetup
gosub debugServerSetup
gosub dataServerSetup


do

   sync

loop

Believe it or not, the server.dba isn't going to get much bigger than that :)

OK, so now when you SAVE ALL and COMPILE and RUN your server, you will get something like this:


OK, it's not thrilling.  But those IDs are critical to the success of your game, and we have the design and foundational code in place now that intrinsically solve the majority of issues that Indy game developers run into with ID management.  Not bad for an hour of code.

Thanks for reading!




Hour 3 - Land and Sky

posted Jul 5, 2010, 2:00 PM by Unknown user   [ updated Feb 24, 2011, 11:49 AM ]

Our current milestone we're working toward is to create a basic network chat client server.  If we follow this route, though, we're going to end up with another boring "cube and grid" demo.  Now it's true that we could add networking to the current BASIC World demo and we'd be technically done with this milestone -- but doing that will NEVER impress a publisher.  IMHO, you should always try to go Above and Beyond the expectations of your publisher, and give them more than they expected for every single milestone.  It won't always be possible, but it's a good goal to have in mind just as long as you clearly define to your team EXACTLY what Above and Beyond means and everyone is working in the same direction :)

You may think that it's odd talking about "publisher expectations" when all I'm doing here is a simple BASIC tutorial - but again, if you really want to get into professional game development, then these are the kinds of things you need to be thinking about.  You might as well get used to thinking along these lines now early in your career so that it can be second-nature later on.

So, for this first milestone, we're going to add in a very basic terrain and skybox system - for the sole purpose of making our project not look absolutely hideous.  

One of the biggest challenges of doing this tutorial is presenting each hour of work in small, manageable "bites."  I don't want to overwhelm people or get them into the mode of thinking, "I'm not even going to try to write this code myself, I'm just going to do the SVN checkout."  While the SVN checkout is always there, you'll learn the most from trying to write this code on your own first, taking a look at the published code if you need to, and finally doing the checkout if you get completely stuck.

Because I'm doing things in small, manageable bites; it may appear that I'm "reworking" code sometimes - but that's necessary to get some basic functionality in place in small pieces like this.  Let's take a look at tonight's hour...  if I were to write the terrain system to right way, here's everything else that I would need to write first:

  • The debug system, to allow logging of an area load
  • The server and client system options and functions to provide pivotal functions and values for all other modules
  • The command system to process an area load command
  • The central data system to contain and define area objects
  • The mesh loading system that could dynamically "re-route" a mesh load request for a terrain and send it on to the terrain system to actually build the terrain object from components
  • And finally the terrain system itself to build the terrain
...and a similar code path would need to exist for the skybox.

That's just too much to all go into a single hour of coding (in fact, all of that is several weeks worth).  So, instead, we're just going to build out the final module and call it directly from the main module.  Of course, this means we'll have to do a little re-writing later, but if we think critically about it, then the re-writing should be a simple change of the function signature.

So, let's get started!

Open your project folder, and add the following folders under your "media folder"

skybox
terrain

Now open your DBPro installation folder and browse down the path "Help --> Advanced Terrain --> Media"  We're going to borrow the media in here for our project to improve the look of our project quickly:

Copy the following files to your project's new /media/skybox/02 folder

1.bmp
2.bmp
3.bmp
4.bmp
5.bmp
6.bmp
skybox2.x

Now copy the following files to your project's new /media/terrain folder

detail.tga
map.bmp
texture.bmp

Rename these files as follows:

02_DT.tga
02_HF.bmp
02_TX.bmp

OK, let's get to the code to get these loaded and built.  Add the following:

cameraClient.dba

cameraClientSetup:

   autocam off
   set camera range 0.5, 30000

return

skyClient.dba

skyboxClientSetup:

   global idSkybox as integer
   #constant SKYBOX_PATH "media/skybox/"

   idSkybox = 1
   rem termporary

return



function skyboxLoad(skyboxID)

   if skyboxID = 0 then exitfunction -1
   meshName$ = "skybox"+str$(skyboxID)+".x"

   `if idSkybox = 0 then idSkybox = dataSharedObjectFindFree()
   `dataObject(idSkybox).rpgid=1
   `dataObject(idSkybox).class = DATA_CLASS_SKYBOX
   rem not yet

   filename$ = SKYBOX_PATH + meshName$

   if file exist(filename$)

      load object filename$, idSkybox
      set object light idSkybox, 0
      set object texture idSkybox, 3, 1
      position object idSkybox, 1000, 2000, 4000
      scale object idSkybox, 30000, 30000, 30000

   else

      `debugWrite(DEBUGWARN,"Skybox not found: " + filename$)
      rem not yet

   endif

endfunction 0

function skyboxUnload()
   if idSkybox = 0 then exitfunction -1
   if object exist(idSkybox) = 0 then exitfunction -2

   delete object idSkybox
endfunction 0

terrainClient.dba


terrainClientSetup:

   #constant TERRAIN_PATH "media/terrain/"

   global idTerrain as integer
   global idTerrainTex as integer
   global idTerrainDet as integer

   idTerrain=2
   idTerrainTex=3
   idTerrainDet=4
   rem temporary

return

function terrainLoad(terrainID, detailID)

   `if idTerrainTex = 0 then idTerrainTex = systemFreeImage()
   `if idTerrainDet = 0 then idTerrainDet = systemFreeImage()
   `if idTerrain = 0 then idTerrain = dataSharedObjectFindFree()
   `dataObject(idTerrain).rpgid=1
   `dataObject(idTerrain).class=DATA_CLASS_TERRAIN
   rem not yet


   texFileName$ = TERRAIN_PATH + "0" + str$(terrainID) + "_TX.bmp"
   mapFileName$ = TERRAIN_PATH + "0" + str$(terrainID) + "_HF.bmp"
   detFileName$ = TERRAIN_PATH + "0" + str$(detailID) + "_DT.tga"

   load image texFileName$, idTerrainTex
   load image detFileName$, idTerrainDet

   make object terrain idTerrain
   set terrain heightmap idTerrain, mapFileName$
   set terrain scale idTerrain, 3, 0.6, 3
   set terrain split idTerrain, 4
   set terrain tiling idTerrain, 4
   set terrain light idTerrain, 1, -0.25, 0, 1, 1, 0.78, 0.5
   set terrain texture idTerrain, idTerrainTex, idTerrainDet
   build terrain idTerrain

   position object idTerrain,0,-5,0
   rem temporary

endfunction

function terrainUnload()


   if idTerrainTex > 0
      if image exist(idTerrainTex)
         delete image idTerrainTex
      endif
   endif

   if idTerrainDet > 0
      if image exist(idTerrainDet)
         delete image idTerrainDet
      endif
   endif

   if idTerrain > 0
      if object exist(idTerrain)
         `collisionDelete(idTerrain)
         rem not yet
         destroy terrain idTerrain
      endif
   endif

   `systemGameModeRemove(SYSTEM_MODE_AREA_TERRAIN)
   rem not yet

endfunction


Now, in your worldofomen.dba, replace this:

REM Point of reference
make matrix 1, 64, 64, 64, 64

with this:

gosub skyboxClientSetup
skyboxLoad(2)

gosub terrainClientSetup
terrainLoad(2,2)


Ahhh, now there is one more thing that needs to be updated -- so here's your challenge for tonight.


Challenge:  The object ID that we're using for our player is no longer valid.  Look through the sky and terrain modules and see what the next valid ID for the player avatar object would need to be to be correct.  Now change this ID wherever it is referenced.

Tonight's challenge will demonstrate the importance of proper ID management.  As you can see, this isn't a task that you EVER want to do manually and you should NEVER hard-code any game object IDs.  Over the next couple of hours, we'll replace those hard-coded IDs with a full ID management system.
  

SAVE ALL and Run it (F5).  If done correctly, you should have something like this:


Soooo much better than looking at a blue screen with a white grid!


Thank you to FoundD for pointing out the following errors in the Commit for tonight.  These errors are fixed in Hour 4:

1. The name of the terrain texture file is "2_TX.jpg", but the file name in the commit is "2_TX.bmp" -- converting the file to JPG format will fix the issue, as will just do a checkout to Hour 4.

2. The camera distance isn't set far enough to see the skybox (or the skybox is too big depending on how you look at it). You can either decrease the scale of the skybox object, or increase the camera distance, or just do a checkout to Hour 4.

Also Thank you to The Wilderbeast for pointing out that the leading zero prefix on the terrain texture files doesn't match up with the code.  I have fixed this above.



Hour 2 - Framework

posted Jul 4, 2010, 12:24 PM by Unknown user   [ updated Jul 5, 2010, 7:41 PM ]

In this hour, we're going to expand the basic framework already built and start dividing up our project into more manageable modules.  The larger you project is, the more divisions you're going to want to make.  This is done for a couple of reasons:  (1) It allows more people to contribute simultaneously, (2) it limits the scope of bugs, and (3) it provides greater isolation for faster debugging.

We're going to divide our modules by object type and by execution environment (shared/client/server).  Now generally speaking, this is a BAD idea the way that most people would expect to implement it.  The last thing you want is to have dozens or arrays for every possible object type with different unique game IDs!

To avoid these problems, all the "data" of our objects is going to be contained in a generic "data" superclass.  All objects in the game will be of this one "dataObject" type.  But the _functions_ for all the different "types" of things in the world will be divided into different modules -- so, we'll have a camera module, a player module, a sky module, etc...  We'll also be dividing these function by execution environment allowing at least these possibilities:
  • Functions that are only available on the client
  • Functions that are only available on the server
  • Functions that are available on both client and server AND work the same way in both environments
  • Functions that are available on both client and server BUT work differently on the client than on the server
  • Functions that are available on both client and server BUT may do absolutely nothing on one or the other
Now, I know some members of the TGC Community may be groaning right now or scratching their heads wondering why I don't use the built-in array pointers in DBPro.  Because, of course, this would save a lot of memory... so they're probably thinking I should do something like this:

type personType
  name as sting
  location as vector
  etc...
endtype

type objectType
  person as personType
  etc...
endtype

The problem with this is that it makes persisting data an absolute nightmare - those pointers aren't going to be valid when you try to re-load them.  Plus, let's look at it another way - what's the most memory I could save by doing this... maybe a MEG total ?  When my _laptop_ has 3 GIGs of RAM, I'm not going to worry about using up an extra GIG or so because my arrays are "sparse" :)  Trust me, it's just not worth all the extra coding overhead.

In addition, making every game object share a single data type gives you LOTS of flexibility to do all kinds of interesting things that players don't expect -- which is ALWAYS a good thing :)  So, enough of my rambling, let's get on to this hour's work.

Open up your "World of OMEN.dbpro" (your "client project"), click "Files" and then keep clicking "Add New" to add all the following code modules in the modules folder IN THIS ORDER (yes, order is important):

  • modules\debugShared.dba
  • modules\debugClient.dba
  • modules\systemShared.dba
  • modules\systemClient.dba
  • modules\commandShared.dba
  • modules\commandClient.dba
  • modules\encryptShared.dba
  • modules\netShared.dba
  • modules\netClient.dba
  • modules\dataShared.dba
  • modules\dataClient.dba
  • modules\mouseClient.dba
  • modules\keyboardClient.dba
  • modules\guiClient.dba
  • modules\cameraClient.dba
  • modules\meshClient.dba
  • modules\collisionClient.dba
  • modules\terrainClient.dba
  • modules\skyClient.dba
  • modules\playerShared.dba
  • modules\playerClient.dba
  • modules\abilityShared.dba
  • modules\inventoryShared.dba
  • modules\goalShared.dba
  • modules\effectClient.dba
  • modules\soundClient.dba
Alight, now SAVE ALL and close.  Now open your server project and add all the following modules (make sure to BROWSE for the "Shared" modules because we already built them and ADD NEW for the new "Server" modules):

  • modules\debugShared.dba
  • modules\debugServer.dba
  • modules\systemShared.dba
  • modules\systemServer.dba
  • modules\commandShared.dba
  • modules\commandServer.dba
  • modules\encryptShared.dba
  • modules\netShared.dba
  • modules\netServer.dba
  • modules\dataShared.dba
  • modules\dataServer.dba
  • modules\playerShared.dba
  • modules\playerServer.dba
  • modules\abilityShared.dba
  • modules\inventoryShared.dba
  • modules\goalShared.dba
Now SAVE ALL and close.

OK, let's go back to our main project folder, and add the following folders:
  • data
  • media
  • server
Under the new "server" folder add the following:
  • data
  •  
You would think that blank files really wouldn't contribute very much to a programming project - but, you'll be surprised how much this will help us as we go along.

Thanks for reading!


Hour 1 - BASIC World

posted Jul 3, 2010, 5:49 PM by Unknown user   [ updated Jul 5, 2010, 7:36 PM ]

Now that we have a design and a plan, we're ready to begin implementing our milestones list.  

Most game development studios "live and die" by their milestones list.  Proving that you have reached a milestone brings (a) publisher payments, (b) a new round a media attention, (c) and keeps studio morale high, among many other things.  For those of you reading this who are interested in a full-time career in game development, you should get used to the practice of "milestone-directed work" - so that's what we'll be doing here.

Our first milestone is "Create basic network chat client/server".  Now of course, this is a little too vague AND there are prerequisites to completing even this milestone.  Let's take some time and do some critical thinking on this to develop a "framework" of sorts to enable us to make in-flight adjustments for our project.

  1. First and foremost, we need to ask ourselves, "How do we know when this goal is done?"  This will create a list of "indicators" that can be used to move to the next milestone with confidence AND provide proof to a publisher that we truly have accomplished what we said we were going to accomplish.

  2. From this list of indicators, we need to ask, "What sub-goals should we define so that we can produce these indicators?"
Doing steps 1 and 2 iteratively will go a long way to helping turn vague milestones into actionable to-do lists.  Let's do this now with the first milestone:

Indicators:  Create basic network chat client/server
  • Client is able to create a connection over the Internet to the server 100% of the time through firewalls, proxy servers, routers, etc... when the server is listening.
  • Server is able to accept a client connection request 100% of the time through same, and client and server can maintain a dialog through this connection.
  • Server or client can disconnect from this connection, and neither will be negatively impacted - both will free the connection cleanly (reconnection is a later goal).
  • Complete dialog on client and server should be visible and it should be possible to alter messages during runtime (this proves to your publisher that you didn't just create some scripted text output)
Sub-Goals: Create basic network chat client/server
  • 1.1  Establish a basic protocol that will be used between client and server.  This should minimally be shared network message IDs.
  • 1.2  Create a listening server that can detect connection attempts using the protocol defined in 1.1, and respond to messages send from client.
  • 1.3  Create a client that will connect to the server, send messages, listen for replies, and post the entire dialog.
  • 1.4  At runtime, both client and server should be able to alter the messages being sent.
  • 1.5 Create a small virtual world with an avatar that can be moved around the world and reflect its location on the server - this is done to make the accomplishment of milestone 1 as EVIDENT as possible.
Now that we've got some sub-goals, I'll add them to our milestones list.

In the future, I won't go into this level of detail concerning creating action lists -- you'll just see the milestones expand as I go :) However, I wanted to go through it once to provide an example of the level of critical thinking that is needed to approach each milestone.  "Real" game studios have thousands or even tens of thousands of individual action items (depending on the size and scope of the project) derived from enormous milestone lists -- just something to keep in mind.

Now, just so we don't bore anyone to tears, we'll begin with goal 1.5 -- this will also give us the opportunity to create some nice screenshots.  Always lead with a video if you can, but in our case we're not going in front of a publisher so screenshots will be sufficient :)

Open the DarkBASIC Pro Editor.



OK, let's start a new project and just to be original, we'll call it "worldofomen" (there are reasons for no spaces that we'll get into later).


Easy enough so far...

So, that's why we'll start changing things around of course :)  

That's right... experience kicks in and we have to start changing things even at Hour 1.  But you'll thank me for this later - honest.

  1. Save your project - yes, this blank project with just a few lines of comment code.
  2. Close DBPro and browse to your /projects folder, which should be at "C:\Program Files\The Game Creators\Dark Basic Professional\Projects\"  
  3. Open the "worldofomen" folder and create a new folder inside it called "modules"
  4. Move the worldofomen.dba file into this "modules" folder
  5. Open the file "worldofomen.dbpro" in your favorite text editor and update the location of the main module so it looks like this (just the relevant section is shown):



  6. Now double-click on "worldofomen.dbpro" to make sure it correctly opens the moved main module.

Create a new project again, but this time name it simply "server."  Repeat steps 1 through 6 with this new server project and then copy the contents of the this server project folder INTO the worldofomen project folder (effectively merging them).  Your final folder should look like this:


OK, let's get some code into the repository to appease the angry, vengeful repository demons!  Open up your worldofomen.dbpro (that's too much to type, from now on I'm just going to call either "the client" or the name of a city in Norway).

OK, add the following code:

Rem Project: worldofomen
Rem Created: Saturday, July 03, 2010

Rem ***** Main Source File *****

sync on : sync rate 60

REM Point of reference
make matrix 1, 64, 64, 64, 64

REM Our avatar
make object cylinder 2,2


do


   control camera using arrowkeys 0,0.1,1
   position object 2, camera position x(0), camera position y(0), camera position z(0)
   rotate object 2, 0, camera angle y(0), 0
   move object 2, 4

sync
loop

Now click SAVE ALL (CTRL-SHIFT-S), and then COMPILE AND RUN (F5).  Now you will have your very own "First Screenshot" that every Indy Game Developer has sitting someone on their hard drive :)


Believe it or not, you've actually accomplished quite a bit this hour.  Let's take a closer look...

  1. You have a rendering engine up and running.
  2. Take a closer look at that sphere as you move around -- yep, it's getting lighted dynamically.  It's vertex lighting, but still - it's dynamic and correct.
  3. You've got a working camera that (seems) to follow around your avatar (actually it's the other way around, but still - not bad).
  4. Your game is accepting input from the keyboard and processing it with no "missed keys" or "overbuffer" problems.
  5. You game runs in a window (Windows Mode) -- these days, most players expect (nay DEMAND) this because they're probably running Some Other Game in another window, Facebook in a third, well you get the idea..
  6. AND we can check off 01.5 as Complete!
GO YOU !!!  Not too shabby for less than an hour of work :)

The easy route...

Every time a post is made to this blog, I'll also be posting an update to the project's Google Code repository.  Frequently, I won't be able to cover absolutely everything in excruciating detail (like I did tonight).  Sometime, even when I do cover every change in the blog, you still may feel... well, a little lost.

SVN to the rescue!  Since I'll be committing changes to the repository in sync with blog posts, to get your code up to date, you can just do the folloiwng:

  1. Create a new folder under your DBPro projects folder named "worldofomen" (you only have to do this once)
  2. Use your favorite SVN client to do a checkout following the SVN Checkout Instructions


Thanks for reading, next hour will be making this world a little more attractive.




Development Workbench

posted Jul 3, 2010, 5:49 PM by Unknown user   [ updated Mar 23, 2011, 9:40 PM ]

Here's the complete list of everything you will need to compile World of OMEN:

  1. An account on The Game Creators forum.  You'll need this to download updates to DarkBASIC Professional itself, and to download community modules.  A forum account is free.
  2. DarkBASIC Professional (obviously).  There is, of course, a free version available.  However, you are going to want to eventually purchase a license, since this will allow you to use all the plug-ins and allow you to upgrade to the latest version.  It's only US $70, which is quite a bargain considering all you are getting.  In either case please watch the Activation Guide at the bottom of the free version download page to get your license activated.
  3. Got that TGC forum account?  Good!  Now go grab the Advanced Terrain Expansion Pack.  It's free.
  4. Now you'll also want IanM's Matrix1Utils collection.  We want these, of course, because the really do have a minty taste - Ian has confirmed it ;)  Again, this is free.
  5. Also get "Sparky's" Sliding Collision DLL (make sure to get version 2.05 or later).  This one is free.
  6. DarkNet - Advanced Networking DLL.  Not free, but VERY reasonably priced considering that you are getting an extremely sophisticated networking DLL that can be used for .NET development, C++ development, and DBPro development - it's only $33 last time I checked.
  7. DarkFish - Advanced Encryption DLL.  This one is free too.

I am currently using DarkBASIC Pro 7.6 - make sure to update your DBPro install to the current version.

Make sure that your compiled executables (by default these are "worldofomen.exe" and "server.exe" have administrative privileges to your project directory and all sub-directories.  In other words, they both need to be able to Create, Change, and Delete files.

Instructions for how to install each DLL are included in the forum and usually in the download itself too.  Usually, this can be summarized as "extract into your DBPro Installation folder and everything will go where it needs to go." :)


That will do it for now, I'll keep this list updated as we go along and I'll specify in each Hour post if something new is needed.


Organization & Planning

posted Jul 3, 2010, 5:48 PM by Unknown user   [ updated Jul 5, 2010, 10:17 AM ]

As with any programming project it's a good idea to set your scope as specifically as possible right from day 1 so everyone knows what they're in for.  So here's a "complete as possible" list of everything that's going to be included in World of OMEN (Frost version) and what's not, and the reasons why.  These are technical features, not to be confused with gameplay features :)

In Scope

  • Character Customization.  Two basic models (male/female), with the ability to customize textures, colors, patterns, and "add-on" meshes (like hair or helmets) for deep customization.  This will help players feel connected to their characters which results in stronger game loyalty and longer play sessions.
  • Multiple Characters per Account.  Players will be able to have multiple characters per player account.  This greatly helps with re-playability and game loyalty because people are much more apt to "roll a new character" to explore different play styles and player abilities if they don't have to delete the "maxed-out" character they spent 6 months developing :)
  • Character Advancement.  Experience points, leveling, gaining new abilities.
  • Multiple Chat Channels.  Local, broadcast, private, team, "guild" and custom channels.  It's a requirement these days, and very handy for on-line meetings with developers.
  • MMO-Style Camera.  Multiple modes will be available like "click to move" and "Mouse Steering" with the ability to change zoom, orbit, etc...  Includes "mini state system" for camera to correctly position the camera to avoid the player being obscured by objects in the line of sight of the camera.
  • Movement Prediction.  Interpolation and extrapolation of movement to make movement of NPC's, enemies, and other players look more natural.
  • Missions System.  Mission givers, multiple-objective missions, advanced complex object types (for example having 2 people throwing switches at the same time).  Includes linked missions ("story arcs").
  • Inventory System.  Players will be able to acquire a limited number of items and keep them in their character's inventory.  There will be many uses for items, including mission and goal completion.
  • Goals System.  Fun "to do" items for the explorers and collectors that will reward the player with different types of "badges" that will be visible to other players.
  • Tokens System.  Allows for granting of "tokens" that cannot be acquired by players through normal gameplay.
  • Teaming.  The ability for players to form teams and complete missions together.
  • Advanced Zoning.  Intra-Area, Intra-Zone, and Inter-Zone instantaneous transportation of character and (optionally) whole team.
  • State Machine AI System.  Simple, programmable state machine to use to develop relatively complex NPC behaviors.
  • Multi-User Simultaneous Editing (MUSE) System.  In-game Editor that allows multiple World Builders to edit an area simultaneously.
  • Co-Routines.  Also called "procs" these are spawned processes that are used to manage complex gameplay functions that cannot be executed on the main thread.
  • Advanced GM Command System.  Each GM command can be selectively granted or suspended for individual GMs.  This allows the implementation of a "GM Leveling" system.

Out of Scope

  • Database Connectivity.  Leaving this out because it's relatively easy to extend your code from file-based system to SQL calls.  There's even (of course) a free community DLL that will handle the database connectivity for you.  Also, I'll make the data access code functions highly compartmentalized so it should be easy enough to add on.  In my experience, the value of keeping most things in text files during development FAR outweighs the benefits of keeping data in a database.  The additional overhead of maintaining the database engine and connectivity to it (and that additional complexity of the code and SQL statements) really will just slow you down.  We'll stick with a file-base approach all the way to the end.
  • Projectiles or "FPS-Style" Targeting or Gameplay.  The math and networking code to make this function correctly is waaaay beyond the scope of an entry-level tutorial.
  • Streaming Assets or Streaming Bundles.  Just not possible with the current state of technology of DBPro.  You can get co-routines with the Matrix1Utils, and you can get inter-process communications, but there's critical functionality missing to make it work.  It's very close, though.
  • Inventions System.  Again, far too complex for an entry-level tutorial - although a good candidate for an addition to the system once all primary In-Scope items are complete.
  • Auction House.  For same reasons as Inventions System.  Also a good candidate for addition later on.
  • Anything Else.  Anything else not specifically listed as In Scope is Out of Scope.

Introduction

posted Jul 3, 2010, 5:48 PM by Unknown user   [ updated Jul 5, 2010, 10:21 AM ]

World of OMEN is a multiplayer online game developed in DarkBASIC.  

This project is a tutorial to develop the third version of OMEN (named the "Frost" version) hour by hour and step by step.  Each hour of development work will be explained in these Project Updates pages, and an SVN repository will be updated in lock-sync with each hour of development.

This is a free tutorial, and the source code for this version of World of OMEN is free as well.  Pre-compiled binaries and the Soruce Code SVN can be accessed from the project links on the left of the page.


31-40 of 42