Game Loop

[ LiB ]

Entities are finally finished, so you can sigh in relief or have a party. Now, I'll show you the marvelous game loop .

The game loop is a concept in SimpleMUD that handles everything that depends on timers. For example, the databases need to be saved at particular time intervals, players need to heal, and enemies need to look for players to attack. The game loop manages all these functions.

NOTE

Event-based systems are really efficient, because they ensures that you do work only when events occur, rather than constantly checking to see if something happened . How ever, there's really no way you can ever go to a complete event based system, which is why a simple game- loop is needed.

Up until this point, the SimpleMUD was purely an event-based system; everything that happened in the game was triggered by something a connected player sent to the game. The game loop adds some independent initiative to the game, so that it will actively perform actions when it needs to, instead of just responding to player input.

The game loop is stored in a class appropriately named GameLoop , which you can find in the files /SimpleMUD/GameLoop.h and .cpp. Here's a listing of the class:

 class GameLoop { public:     GameLoop()      { LoadDatabases(); }     ~GameLoop()     { SaveDatabases(); }     void LoadDatabases();       // load all databases     void SaveDatabases();       // save all databases     void Load();                // load gameloop data     void Save();                // save gameloop data     void Loop();                // perform one loop iteration     void PerformRound();        // perform combat round     void PerformRegen();        // perform enemy regen round     void PerformHeal();         // perform healing round protected:        BasicLib::sint64 m_savedatabases;     BasicLib::sint64 m_nextround;     BasicLib::sint64 m_nextregen;     BasicLib::sint64 m_nextheal; }; 

First, take a look at the constructor and destructor. They call the LoadDatabases and SaveDatabases functions respectively. So whenever you create a GameLoop object, all of the databases are automatically loaded from disk, and whenever a GameLoop object is destroyed , all the databases are saved to disk. You'll see this in action when I show you Demo10-01.cpp later on in this chapter.

Next look at the data: There are four 64-bit integers, representing four different times. These four times are in system time (which is kept by the Game::s_timer timer object), and indicate times when the following events should occur: the next time the databases save, the next combat round, the next monster regen, and the next player healing.

I'll get more into system timing in a little bit.

NOTE

System time is kept in milliseconds , starting from 0, inside the Game::s_timer object. For example, a system time of 10,000 ticks would mean that the system has been running for 10,000 ticks / 1000 ticks/ second = 10 seconds. If GameLoop::m_savedatabases is 900,000 ticks (900 seconds, or 15 minutes), that means that the system will save the databases 15 minutes after the system has started.

Loading and Saving Databases

When the database loading/saving functions are called, they are designed to automatically load or save every database that needs to be loaded or saved to disk. For example, the GameLoop::LoadDatabase function is as follows :

 void GameLoop::LoadDatabases() {     Load();     ItemDatabase::Load();     PlayerDatabase::Load();     RoomDatabase::LoadTemplates();     RoomDatabase::LoadData();     StoreDatabase::Load();     EnemyTemplateDatabase::Load();     EnemyDatabase::Load(); } 

The Load function deals with loading the four system-time variables from disk.

Saving is similar, but since not all databases need to be saved back to disk, the function is simpler:

 void GameLoop::SaveDatabases() {     Save();     PlayerDatabase::Save();     RoomDatabase::SaveData();     EnemyDatabase::Save(); } 

In addition to the time variables saved with the GameLoop::Save function, only players, rooms, and enemies need to be saved to disk.

Loading and Saving Time Variables

The time variables are saved within a text file entitled game.data. When you start your MUD for the first time, the game time starts at 0 and increases based on how long it's running. When the game has been up for a period of time, and is then shut down, the MUD should save the time at which it was sent to disk, so you know what time it is when you turn the game back on.

Saving

The GameLoop::Save function saves the time variables to disk:

 void GameLoop::Save() {     std::ofstream file( "game.data" );     file << "[GAMETIME]      ";     insert( file, Game::GetTimer().GetMS() ); file << "\n";     file << "[SAVEDATABASES] ";     insert( file, m_savedatabases );          file << "\n";     file << "[NEXTROUND]     ";     insert( file, m_nextround );              file << "\n";     file << "[NEXTREGEN]     ";     insert( file, m_nextregen );              file << "\n";     file << "[NEXTHEAL]      ";     insert( file, m_nextheal );               file << "\n"; } 

I've used the BasicLib::insert function to insert the times into the text file (due to the fact that VC6 does not stream 64-bit integers properly). As you can see, the function essentially writes the current system time, and then the four other time variables. Here's a sample listing of the datafile:

 [GAMETIME]      3917661 [SAVEDATABASES] 4500000 [NEXTROUND]     3918000 [NEXTREGEN]     3970000 [NEXTHEAL]      3960000 

Since the times are millisecond-based, you can tell that the server represented by this file has been up for about an hour , and that the next round will occur in less than one second from the current time (3918000 minus 3917661 = 339, so in 339 milliseconds, the next combat round for enemies will occur), and that the database is due to be saved in 9 minutes.

Loading

Loading the variables is slightly more complicated. In a "clean" installation of SimpleMUD, the game.data text file won't exist, so all five timers must be reset to the default values.

First, let me show you the default value definitions:

 sint64 DBSAVETIME = minutes( 15 ); sint64 ROUNDTIME  = seconds( 1 ); sint64 REGENTIME  = minutes( 2 ); sint64 HEALTIME   = minutes( 1 ); 

These values determine how much time should pass between certain actions. For example, the databases should be saved every 15 minutes (the seconds and minutes helpers are in the BasicLib), combat rounds occur every second, monsters regen every two minutes, and players heal once a minute. I refer to these values as delta values. (Delta is a mathematical term commonly associated with change.)

Now the loading function:

 void GameLoop::Load() {     std::ifstream file( "game.data" );  // open file     file >> std::ws;                    // eat whitespace     if( file.good() ) {                 // detect if file is good         std::string temp;         sint64 time;         file >> temp;   extract( file, time ); // read system time         Game::GetTimer().Reset( time );        // reset timer to that time         // read other variables:         file >> temp;   extract( file, m_savedatabases );         file >> temp;   extract( file, m_nextround );         file >> temp;   extract( file, m_nextregen );         file >> temp;   extract( file, m_nextheal );     } 

The previous code segment is executed when the game.data file exists. It loads in the current system time, and resets the Game 's timer object to the current system time, and then proceeds to load the other variables.

If the file is corrupted, however, this code is executed:

 else {         Game::GetTimer().Reset();         m_savedatabases = DBSAVETIME;         m_nextround = ROUNDTIME;         m_nextregen = REGENTIME;         m_nextheal = HEALTIME;     }     Game::Running() = true; } 

The game timer is reset to 0, and the four time values are set to their deltas, meaning that the first database saving will occur at 15 minutes, and so on.

Finally, the game is told to start running.

The Loop

In the SimpleMUD, the GameLoop::Loop is called once every cycle, and this function takes care of the various time-based activities:

 void GameLoop::Loop() {     if( Game::GetTimer().GetMS() >= m_nextround ) {         PerformRound();         m_nextround += ROUNDTIME;     }     if( Game::GetTimer().GetMS() >= m_nextregen ) {         PerformRegen();         m_nextregen += REGENTIME;     }     if( Game::GetTimer().GetMS() >= m_nextheal ) {         PerformHeal();         m_nextheal += HEALTIME;     }     if( Game::GetTimer().GetMS() >= m_savedatabases ) {         SaveDatabases();         m_savedatabases += DBSAVETIME;     } } 

The function checks the four timer variables to see if their time has come. For example, if m_nextround is 4534000, and the timer is at 4534013 (13 milliseconds later), the PerformRound function is called, and ROUNDTIME (1 second, or 1000 milliseconds) is added to m_nextround , making it 45345000. Since the timer is not guaranteed to be called on 1-millisecond boundaries, you must check to see if the timer has gone past the time at which one of the actions was set to execute. The function does the same for the other three time variables.

NOTE

This is an inefficient method of timing, but it serves its purpose in SimpleMUD. Imagine if you had more than four different timed events. The function would become large and wasteful in terms of processing power. It gets even worse when you want enemies to have different attack times, instead of all enemies in the game attacking on the same game loop. I will be exploring much better methods of utilizing timers in the next part of the book.

Performing the Combat Round

Whenever the game decides to perform a combat round, the GameLoop::PerformRound function is called. This function simply goes through every enemy in the game, and if the enemy sees a player in the same room, it is told to attack:

 void GameLoop::PerformRound() {     EnemyDatabase::iterator itr = EnemyDatabase::begin();     sint64 now = Game::GetTimer().GetMS();     while( itr != EnemyDatabase::end() ) {         if( now >= itr->NextAttackTime() &&    // make sure enemy can attack             itr->CurrentRoom()->Players().size() > 0 ) // check players             Game::EnemyAttack( itr->ID() );    // tell enemy to attack         ++itr;     } } 

For every enemy, two things are checked: Can the enemy attack, and are there any players in the room? Enemies keep track of the next time they are allowed to attack, which is based on which weapon they are using. You'll see how this works when I show you the Attack function.

If there are no players in the room, there's obviously no one for the enemy to attack, so the function doesn't do anything.

Regenerating Enemies

Every two minutes, the game performs an enemy regeneration cycle, in which it tries to put more enemies into the rooms that have too few. As you saw in Chapter 7, rooms can have one type of enemy, and each room can have a maximum number of enemies per room.

The process for this entails looping through every room, and determining if a new enemy should be spawned:

 void GameLoop::PerformRegen() {     RoomDatabase::iterator itr = RoomDatabase::begin();     while( itr != RoomDatabase::end() ) {         if( itr->SpawnWhich() != 0 &&    // make sure room can spawn enemies             itr->Enemies().size() < itr->MaxEnemies() ) // don't overflow         {             // tell the database to create a new enemy in the room             EnemyDatabase::Create( itr->SpawnWhich(), itr->ID() );             Game::SendRoom( red + bold + itr->SpawnWhich()->Name() +                             " enters the room!", itr->ID() );         }         ++itr;     } } 

It's not a complex process; it uses the EnemyDatabase::Create function to create enemies in each room that needs them. The loop checks two conditions per room: First, it determines if enemies should be created (because rooms with a SpawnWhich ID of 0 shouldn't spawn any enemies), and second, it decides if more enemies can fit into the room.

If these conditions are met, a new enemy is created, and a message is sent to the room stating that the enemy has entered the room.

NOTE

This is also an inefficient process, but it works for SimpleMUD. In the next part of the book, I explore more efficient methods of filling your realm with enemies, as well as other things, such as the impact on the economy. If you think about it, an endless supply of enemies means an endless supply of loot, and therefore an endless cycle of inflation.

Regenerating Health

It is typical in MUD-like games for users to have regenerating health. Having to constantly buy healing potions can get annoying to your players after a while, so regenerating is usually used as a solution.

Once every minute, the game goes through all the players, and if they are logged in, the players' hitpoints are regenerated:

 void GameLoop::PerformHeal() {     PlayerDatabase::iterator itr = PlayerDatabase::begin();     while( itr != PlayerDatabase::end() ) {         if( itr->Active() ) {             itr->AddHitpoints( itr->GetAttr( HPREGEN ) );             itr->PrintStatbar( true );            }         ++itr;     } } 

As you can see, the player's HPREGEN attribute is added to his hitpoints, and his statbar is updated.

NOTE

Note that true is passed into the PrintStatbar function. When the parameter is true , it tells the game that this is just a trivial update of the hitpoints, so if the player is busy typing something into his Telnet console, the hitpoint update doesn't overwrite what he's typing. The function essentially does nothing when there is something in the player's input buffer.

Attacking and Dying

Finally, we reach the best part of the game controlling what happens when enemies attack and when they kill players. Even though these functions are called by the GameLoop , they reside within the Game class, so I can keep the logic in one central place.

Here are the declarations of the two functions that control this:

 void EnemyAttack( enemy p_enemy ); void PlayerKilled( player p_player ); 

The Attack function tells the game that p_enemy should find a player and attack him. The Killed function tells the game that p_player has died.

When Enemies Attack!

The Attack function follows the basic combat rules I defined in Chapter 7. Before I get into that, however, the enemy first needs to figure out who he wants to attack. This is handled with the assistance of the std::advance function, which takes an iterator, and moves it forward however many places you specify:

 void Game::EnemyAttack( enemy p_enemy ) {     Enemy& e = *p_enemy;      // get enemy     room r = e.CurrentRoom(); // get room     std::list<player>::iterator itr = r->Players().begin();     std::advance( itr, BasicLib::RandomInt( 0, r->Players().size() - 1 ) ); 

Basically, the previous code skips to a random player in the room, which means that the enemy attacks a random person every round. Although these random attacks are not realistic, I did this for "security" reasons: It prevents people from leaving the room, letting the enemy concentrate on someone else in the room, and then come back in.

The next part of the code calculates how much damage is done by the enemy's current weapon:

 Player& p = **itr;       // get player     sint64 now = Game::GetTimer().GetMS();        int damage;     if( e.Weapon() == 0 ) {  // fists, 1-3 damage, 1 second swingtime         damage = BasicLib::RandomInt( 1, 3 );         e.NextAttackTime() = now + seconds( 1 );     }     else {   // weapon, damage, and swing time based on weapon attributes         damage = BasicLib::RandomInt( e.Weapon()->Min(), e.Weapon()->Max() );         e.NextAttackTime() = now + seconds( e.Weapon()->Speed() );     } 

If the enemy doesn't have a weapon, it is assumed the enemy will damage things in the 13 range, with a swing time of one second. (This means that the enemy can attack again in one second.) If the enemy is using a weapon, the damage range and swing time are taken directly from that enemy's weapon object.

Next, the game figures out if the enemy hit the player.

 if( BasicLib::RandomInt( 0, 99 ) >= e.Accuracy() -         p.GetAttr( DODGING ) )     {         Game::SendRoom( white + e.Name() + " swings at " + p.Name() +                         " but misses!", e.CurrentRoom() );         return;     }     damage += e.StrikeDamage();     damage -= p.GetAttr( DAMAGEABSORB );     if( damage < 1 )         damage = 1; 

The code generates a random number from 0 to 99, and if the accuracy of the enemy minus the dodging of the player is more than or equal to the random number, the enemy missed.

For example, an enemy has an accuracy of 80, and the player has a dodging level of 20. Since 80 20 = 60, if the random number is between 60 and 99, the enemy misses. That means that the enemy hits the player about 60% of the time. If the enemy misses, the function returns.

NOTE

Calculating whether the enemy hit a player could have been accomplished earlier, but it turns out that waiting until this point saves code. No matter what, when an enemy swings, its next attack time should be increased. So while you're calculating the next attack time, why not calculate the damage as well?

If the enemy does hit the player, the enemy's strike damage is added to the overall damage value, and the player's damage absorption is subtracted. This means that if the damage was 5, the enemy's SD is 1, and the player's DA is 3, then the actual damage dealt is 5 + 1 3 = 3. Finally, the damage is checked to see if it is below 1, and if so, it's reset to 1.

You don't want enemies doing negative damage (that is, adding hitpoints), or 0 damage to players (which would mean that the enemy missed, but we've already determined that he hit the player).

Here's the final chunk of code:

 p.AddHitpoints( -damage );     Game::SendRoom( red + e.Name() + " hits " + p.Name() + " for " +                     tostring( damage ) + " damage!", e.CurrentRoom() );     if( p.HitPoints() <= 0 )         PlayerKilled( p.ID() ); } 

The damage is removed from the player, and the room is told that the player got hit. Finally, if the player's hitpoints are 0 or lower, the game is notified that the player was killed.

Live and Let Die

As in real life, everyone dies someday. In a dangerous world such as the SimpleMUDs, death comes often if you're not careful.

Whenever a player is killed, the Game::PlayerKilled is called, and the unfortunate player is penalized . A bunch of things need to happen, so I'm splitting up the code into segments:

 void Game::PlayerKilled( player p_player ) {     Player& p = *p_player;      // get the player     Game::SendRoom( red + bold + p.Name() + " has died!", p.CurrentRoom() );     money m = p.Money() / 10;   // calclate how much money to drop     if( m > 0 ) {         p.CurrentRoom()->Money() += m;         p.Money() -= m;         Game::SendRoom( cyan + "$" + tostring( m ) +                         " drops to the ground.", p.CurrentRoom() );     } 

The function in this code segment tells the room that a player has died, calculates how much money to drop, and then drops it.

The next part of code drops a random item:

 if( p.Items() > 0 ) {      // make sure the player has an item         // loop through random indexes until you hit a valid item:         int index = -1;         while( p.GetItem( index = RandomInt( 0, PLAYERITEMS - 1 ) ) == 0 );         item i = p.GetItem( index );     // get the item to drop         p.CurrentRoom()->AddItem( i );   // add it to the room         p.DropItem( index );             // remove it from the player         Game::SendRoom( cyan + i->Name() + " drops to the ground.",                         p.CurrentRoom() );     } 

The function essentially performs a "random bounce" to find an inventory item to drop. This is somewhat awkward , but it's simple and it works for the SimpleMUD. Just keep in mind that this could take a while to find an item, if the player isn't carrying much.

After an item is found, the item is added to the room and removed from the player's inventory, and the room is told that the item was dropped.

The final part of the code subtracts 10% experience, and moves the player to Town Square:

 int exp = p.Experience() / 10;     p.Experience() -= exp;   // subtract 10% exp     p.CurrentRoom()->RemovePlayer( p_player );     p.CurrentRoom() = 1;     // move player to room 1     p.CurrentRoom()->AddPlayer( p_player );     // set player HP to 70%     p.SetHitpoints( (int)(p.GetAttr( MAXHITPOINTS ) * 0.7) );     // send messages:     p.SendString( white + bold +                   "You have died, but have been ressurected in " +                   p.CurrentRoom()->Name() );     p.SendString( red + bold + "You have lost " + tostring( exp ) +                   " experience!" );     Game::SendRoom( white + bold + p.Name() +                     " appears out of nowhere!!" , p.CurrentRoom() ); } 

The player's hitpoints are reset to 70% of maximum. You obviously don't want the player to be ressurected with 0 (or less!) hitpoints, but you also don't want the player to start off with full hitpoints either, so 70% is a good tradeoff .

The last thing the function does is print out messages to the player and to the people in the room in which the player respawns, telling them about his entrance .

[ LiB ]


MUD Game Programming
MUD Game Programming (Premier Press Game Development)
ISBN: 1592000908
EAN: 2147483647
Year: 2003
Pages: 147
Authors: Ron Penton

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net