Chapter VIII

Multiplayer Movement

 

  Now that we have are receiving the forces from the clients, we can actually do each player's movement on the server. To start with we will do everything on the server and then later in Chapter X will will switch the animation to be done locally on the clients.

  Since we already took care of the force updates, this chapter shouldn't be too different from what you would do if you were programming a normal single-player game. So let's get to it. It should be fun to finally see something move. Finally we shall be rewarded for all of our preparation of the earlier chapters.

 

Adding Animation with Blending

  First we will add the animation code with blending. Blending is where you are switching from one animation state to another and instead of just starting on the first animation frame of the next animation, you blend into it. This makes the transition look much smoother.

  Animation blending could be a tutorial in itself and I am not going to go into great detail on how it is done, but if you study the code you might be able to grasp what is going on. I decided to go ahead and add animation blending because I thought a lot of beginner programmers would atleast like to see some code that makes it happen and it makes the game look better.

  So let's create a animation function to handlle animation and then call it once from function move_player. This function continually runs itself doing animation for an entity until the entity is removed. So add this function above function move_player.

 

//--------------------------------------------------------------------

// Function Animate(): animate a entity

//--------------------------------------------------------------------

function animate()

{

proc_late(); // make sure forces set before we run animation

 

while(my)

{

if (my.animation_state == ANIMATION_STATE_RUN)

{

my.current_anim_percent += PLAYER_ANIM_RUN_SPEED * time;

}

else

{

my.current_anim_percent += PLAYER_ANIM_WALK_SPEED * time;

}

 

if( my.current_anim_percent > 100 )

{

my.current_anim_percent -= 100;

}

 

// do animation of current animation state

if (my.animation_state == ANIMATION_STATE_WALK)

{

if(my.prev_anim_state != my.animation_state)

{

my.blend_anim_percent += PLAYER_ANIM_BLEND_SPEED * time;

my.blend_anim_percent = min(my.blend_anim_percent,100);

 

ent_blend("walk",0,my.blend_anim_percent);

 

if(my.blend_anim_percent == 100)

{

my.current_anim_percent=0;

 

ent_animate(me,"walk",my.current_anim_percent,ANM_CYCLE );

my.prev_anim_state = my.animation_state;

my.blend_anim_percent = 0;

}

}

else

{

ent_animate( me, "walk", my.current_anim_percent, ANM_CYCLE);

}

}

else

{

if (my.animation_state == ANIMATION_STATE_STAND)

{

if(my.prev_anim_state != my.animation_state)

{

my.blend_anim_percent += PLAYER_ANIM_BLEND_SPEED * time;

my.blend_anim_percent = min(my.blend_anim_percent,100);

 

ent_blend("stand",my.current_anim_percent,my.blend_anim_percent);

if(my.blend_anim_percent == 100)

{

my.current_anim_percent=0;

 

ent_animate(me,"stand",my.current_anim_percent,ANM_CYCLE );

my.prev_anim_state = my.animation_state;

my.blend_anim_percent = 0;

}

}

else

{

ent_animate( me, "stand", my.current_anim_percent, ANM_CYCLE );

}

}

else

{

if (my.animation_state == ANIMATION_STATE_RUN)

{

if(my.prev_anim_state != my.animation_state)

{

my.blend_anim_percent += PLAYER_ANIM_BLEND_SPEED * time;

my.blend_anim_percent = min(my.blend_anim_percent,100);

 

ent_blend("run",my.current_anim_percent,my.blend_anim_percent);

if(my.blend_anim_percent == 100)

{

my.current_anim_percent=0;

 

ent_animate(me,"run",my.current_anim_percent,ANM_CYCLE );

my.prev_anim_state = my.animation_state;

my.blend_anim_percent = 0;

}

}

else

{

ent_animate( me, "run", my.current_anim_percent, ANM_CYCLE );

}

}

else // attack

{

if(my.prev_anim_state != my.animation_state)

{

my.blend_anim_percent += PLAYER_ANIM_BLEND_SPEED * time;

my.blend_anim_percent = min(my.blend_anim_percent,100);

 

ent_blend("attack",my.current_anim_percent,my.blend_anim_percent);

 

if (my.blend_anim_percent == 100)

{

my.current_anim_percent=0;

 

ent_animate(me,"attack",my.current_anim_percent,ANM_CYCLE );

my.prev_anim_state = my.animation_state;

my.blend_anim_percent = 0;

}

}

else

{

ent_animate( me, "attack", my.current_anim_percent, ANM_CYCLE );

}

}

}

}

 

wait(1);

}

}

 

//--------------------------------------------------------------------

// MOVE - ATTACHED TO PLAYER IN CREATE_PLAYER() FUNCTION

//--------------------------------------------------------------------

action move

{

 

    First we used proc_late(), which moves the current function to the very end of the function scheduler list. Necessary when the execution order of functions within one frame cycle is important. We want to do this function last to make sure the forces have been received and our animation for this frame has been set before animating.

   Ok, here's the brief explaination of the rest of the function. First the function get's next animation frame percent keeping it is between 1 and 100. No, you can not give 110% here like you do at work.

if (my.animation_state == ANIMATION_STATE_RUN)

{

my.current_anim_percent += PLAYER_ANIM_RUN_SPEED * time;

}

else

{

my.current_anim_percent += PLAYER_ANIM_WALK_SPEED * time;

}

 

if( my.current_anim_percent > 100 )

{

my.current_anim_percent -= 100;

}

  Then it checks for what this entity's current animation state is and goes into the correct code for that animtion state. Then it checks if the current animation state is the same as last frame, if it is it just does regular animation. If the animation state was different then last frame, then it starts blending animation (one frame at a time) until the animation blending is done. At the end of each animation frame it save the current animation state into previous animation state for the next frame so we can check if the animation has changed and we need to blend next time.

  I told you it could be a tutorial in itself. You really don't need to concern yourself with the how the animation blending works right now, but later on you might want to re-visit it or find a tutorial on it or atleast some examples from forums if you can understand what it is doing.

 Ok, let's add the call to the animate() function above the while(1) loop in function move_player. This will only be called by a host or single player mode connection. The client connection will do his animation locally and the dedicated server doesn't need to animate entities locally since he can't see them anyway.

 

// if Host animate for Host or single player, dedicated server needs not animate for itself

if ((connection == 3) || (connection == 0))

{

animate(); // animate entity

}

 

while(1)

{

 

  You might have noticed we added quite a few new skills to handle the animation, as well as some new defines for our possible animation states and to control the animation speed. Lets add them to our declarations now. First under Defines add.

 

// Profession Types

define PROF_NUCLEAR_SCIENTIST, 1; // Nuclear scientist

define PROF_BIOLOGICAL_SCIENTIST, 2; // Biological scientist

define PROF_OPERATIONS_OFFICER, 3; // Operations officer

 

// Animation States

define ANIMATION_STATE_STAND, 0;

define ANIMATION_STATE_WALK, 1;

define ANIMATION_STATE_RUN, 2;

define ANIMATION_STATE_ATTACK, 3;

 

// Animation Speeds

define PLAYER_ANIM_WALK_SPEED, 6; // animation speed for walk

define PLAYER_ANIM_RUN_SPEED, 8; // animation speed for run

define PLAYER_ANIM_BLEND_SPEED, 10; // speed for blend

 

 Then add our new animation skill defines under Skill Definitions.

 

define force_x, skill8; // current forces for this ent

define force_y, skill9;

define force_z, skill10;

define current_anim_percent, skill11; // current animation percent

define blend_anim_percent,skill12; // blending animation percent

define animation_state, skill13; // animation state

define prev_anim_state, skill14; // previous animation state for blending

 

Note: It would be a good idea to keep the possible animation states of all your entities the same. If you have a bunch of entities that have different animation names or types your animation code could get out of control very quickly. This is why I changed some of the animation names on the Cbabe model to match the Guard's and Warlock's animation names.

 

  Since we haven't ran the program for a while, go ahead and save the wdl from SED and then test run it on your primary computer. When you get the player visible he should start animating after a short delay (the delay is due to the high latency safety code (sleeps) we added). I wanted you to run the program now because you might have made some errors in the previous two chapters that might need fixed now also.

 

Brain Teaser 4: Why did we place the function animate() above the action move and not below it?

Answer in Appendix V.

 

Initializing the Entity to the Correct Profession

  Ok, let's get the entity set up according to what profession the player selected. First we want to add some hight latency saftey code again like we did earlier int the create_player() function. We do this just to make sure we give the connection enough time to get the entity to all the clients before we start changing values for the entity. I went ahead and put sleep(.3) in this time again. It may however need to be set to sleep(3) for high latency, low bandwidth connections. So you need to play with this value depending on what your game specifications for your internet connect is. Add this code to function move_player.

 

my.player_number = number_of_players; // set the player_number skill

send_skill(my.player_number, SEND_ALL); // send player_number skill to all clients

 

// this code is to solve high latency creation problem

sleep(.3); // this can be left at .3 no matter what

ent_sendnow(my);

sleep(.3); // sleep(3); // high latency solution for now

 

  Now that we have the latency code in, let's set up our player's entity's skills depending on what profession he selected. First let's add some new defines for health, AC, player speed and weapons. We get these values from our game documentation in Chapter II. Add these defines in our declarations.

 

// Profession Types

define PROF_NUCLEAR_SCIENTIST, 1; // Nuclear scientist

define PROF_BIOLOGICAL_SCIENTIST, 2; // Biological scientist

define PROF_OPERATIONS_OFFICER, 3; // Operations officer

 

// Profession speeds

define PROF_NUCLEAR_SPEED, 11; // Speed of nuclear scientist

define PROF_BIOLOGICAL_SPEED, 12; // Speed of biological scientist

define PROF_OFFICER_SPEED, 13; // Speed of operations officer

 

// Profession's max health

define PROF_NUCLEAR_MAX_HP, 50;

define PROF_BIOLOGICAL_MAX_HP, 75;

define PROF_OFFICER_MAX_HP, 100;

 

// Profession's Armor Classes

// if player is hit by projectile, roll 20 sided die, if d20 >= AC, do damage

define PROF_NUCLEAR_AC, 4;

define PROF_BIOLOGICAL_AC, 6;

define PROF_OFFICER_AC, 8;

 

// Movement Defines

define PLAYER_SPEED_TURN, 6; // how fast a player rotates

 

// Weapon Types

define WEAPON_NUCLEAR_ROD, 1; // Nuclear Scientist weapon

define WEAPON_PHOTOSYNTHESIS_EMULATOR, 2; // Biological Scientiest weapon

define WEAPON_COMPRESSION_RIFLE, 3; // Operation Officer weapon

 

 

  Ok, now let's use these defines to set the values for the entity depending the profession the player chose. Add this script into function move_player under our latency safety code.

 

// this code is to solve high latency creation problem

sleep(.3); // this can be left at .3 no matter what

ent_sendnow(my);

sleep(.3); // sleep(3); // high latency solution for now

 

// if not client connection

if(connection != 2)

{

if (my.profession == PROF_NUCLEAR_SCIENTIST)

{

my.speed = PROF_NUCLEAR_SPEED; // set player speed

my.health = PROF_NUCLEAR_MAX_HP; // set health

my.max_health = PROF_NUCLEAR_MAX_HP; // set max health

my.armor_class = PROF_NUCLEAR_AC; // set AC

// send health, max-health & armor_class

send_skill(my.health, SEND_VEC+SEND_ALL);

 

my.weapon = WEAPON_NUCLEAR_ROD; // set weapon type

}

 

if (my.profession == PROF_BIOLOGICAL_SCIENTIST)

{

my.speed = PROF_BIOLOGICAL_SPEED; // set player speed

my.health = PROF_BIOLOGICAL_MAX_HP; // set health

my.max_health = PROF_BIOLOGICAL_MAX_HP; // set max health

my.armor_class = PROF_BIOLOGICAL_AC; // set AC

// send health, max-health & armor_class

send_skill(my.health, SEND_VEC+SEND_ALL);

 

my.weapon = WEAPON_PHOTOSYNTHESIS_EMULATOR; // set wepaon

}

 

if (my.profession == PROF_OPERATIONS_OFFICER)

{

my.speed = PROF_OFFICER_SPEED; // set player speed

my.health = PROF_OFFICER_MAX_HP; // set health

my.max_health = PROF_OFFICER_MAX_HP; // set max health

my.armor_class = PROF_OFFICER_AC; // set ac

// send health, max-health & armor_class

send_skill(my.health, SEND_VEC+SEND_ALL);

 

my.weapon = WEAPON_COMPRESSION_RIFLE; // set weapon type

}

}

 

  Ok let's just look at the if (my.profession == PROF_NUCLEAR_SCIENTIST) statement since all of the if statements really do the same thing, but are just setting different values depending on what profession was selected by player.

  The first thing we do is check to make sure our connection is dedicated server, host or single player (not client).

// if not client connection

if(connection != 2)

  Then we check if the profession that the player chose wa a nuclear scientist.

if (my.profession == PROF_NUCLEAR_SCIENTIST)

  If the player chose to be a nuclear scientist we set his speed, health, max_health, and armor class to the values specified in our game documentation.

my.speed = PROF_NUCLEAR_SPEED; // set player speed

my.health = PROF_NUCLEAR_MAX_HP; // set health

my.max_health = PROF_NUCLEAR_MAX_HP; // set max health

my.armor_class = PROF_NUCLEAR_AC; // set AC

  Now we send the player's health, max_health, and armor_class to all the clients since these values may need to be displayed or used locally on the clients. We send them as a vector so we send them all at once instead of one at a time (See note below). We don't need to send speed since it is only going to be used by the server itself in this tutorial's code, meaning, we are going to do our movement on the server not the clients. This is how standard 3DGS protocol is set up. The server does the movement and then sends information internally to the clients clones of the entity.

// send health, max-health & armor_class

send_skill(my.health, SEND_VEC+SEND_ALL);

 

Note: The reason we can send the health, max_health, and armor_class skills as a vector is because we have them grouped together in our skills. Health is skill 5, max_health is skill 6, and armor_class is skill 7. When you send skills as a vector you send the first skill specified and the following two skills. I actually grouped these three skills together so we could send them as a vector instead of sending them one at a time using three send_skill() commands. This is a more effiecient method to consider when you have to send multiples of 3 skills. You can kill 3 birds with one stone, so to speak.

 

  After that, we go ahead and specify the weapon this entity is carrying.

my.weapon = WEAPON_NUCLEAR_ROD; // set weapon type

  We do the same thing for all professions, the only differnce being the values we set.

 

Adding the Movement Code

  It's time to use the force values we calculated in our input_scan function. We already have everything prepared well so the actual movement code shouldn't be too difficult. Go ahead and add these lines of code in the while(1) loop within function move_player.

 

// if Host animate for Host or single player, dedicated server needs not animate for itself

if ((connection == 3) || (connection == 0))

{

animate(); // animate entity

}

 

while(1)

{

 

// intially set animation to walk, this will be changed below if necessary

my.animation_state = ANIMATION_STATE_WALK;

 

// if moving forward or backwards at walking rate, set animation to walk

if((abs(my.force_x) > 0) && (abs(my.force_x) <= 1))

{

my.animation_state = ANIMATION_STATE_WALK;

}

else

{

// if moving forward or backwards at run, set animation to run

if(abs(my.force_x) > 1)

{

my.animation_state = ANIMATION_STATE_RUN;

}

else

{

// if entity is not turning, then all that is left is standing

if (my.force_y == 0)

{

my.animation_state = ANIMATION_STATE_STAND;

}

}

}

 

  First we set the animation to walking, it will be changed by the if statements if the player is not walking.

my.animation_state = ANIMATION_STATE_WALK;

  Next we check if the player is walking forward or backwards (this is why the abs() value is there, incase he is walking backwards we still want walk animation). A player is considered to be walking if his force_x value is grater than zero and less than or equal to one.

if((abs(my.force_x) > 0) && (abs(my.force_x) <= 1))

{

my.animation_state = ANIMATION_STATE_WALK;

}

  If the player is not moving forwrd or backwar at a walk rate, then we check if he is running. Running is when his force_x value is greater than one.

if(abs(my.force_x) > 1)

  If the player is not running, then we check if he is turning, if he is not turning we know he is standing. If he is turning are intial walking animation we set at the beginning of aall this script will still be set at the walk animation.

if (my.force_y == 0)

{

my.animation_state = ANIMATION_STATE_STAND;

}

 

  Now right under that code let's add the rotation and movement code.

 

if (my.force_y == 0)

{

my.animation_state = ANIMATION_STATE_STAND;

}

 

// get new pan

my.pan = my.force_y * PLAYER_SPEED_TURN * time + my.pan;

 

// use time & speed_forwards_factor

force.x = my.force_x * my.speed * time;

force.y = 0;

force.z = 0;

 

// A5 move

move_mode = glide + ignore_passable;

ent_move(force.x,nullvector);

 

// move the player, c_move bugged at time of release so using ent_move instead

//c_move(my, force.x, nullvector, glide + ignore_passable);

 

// A6.3 feature my.floor_dist to scan the floor and to set entity onto the floor

if(MY.FLOOR_DIST > (MY.MAX_Z - MY.MIN_Z)/2)

{

MY.Z -= MY.FLOOR_DIST - (MY.MAX_Z - MY.MIN_Z)/2;

}

 

/* A5 scan floor;

// scan for floor if we are moving

if (my.force_x || my.force_y || my.force_z)

{

//Scan floor pre A6.30

trace_mode = IGNORE_SPRITES + IGNORE_PASSENTS + IGNORE_PASSABLE + IGNORE_MODELS + USE_BOX;

vec_set(vecFrom,my.X);

vec_set(vecTo,my.X);

vecTo.z -= 400;

RESULT = TRACE(vecFrom,vecTo);

my.Z = TARGET.Z + ((my.MAX_Z - my.MIN_Z)/2);//Set to the floor

}

*/

 

  First we figure the players new pan using our set turn speed and the force_y value.

my.pan = my.force_y * PLAYER_SPEED_TURN * time + my.pan;

  After that, we get are forward speed factor using this entity's speed and force_x values.

force.x = my.force_x * my.speed * time;

force.y = 0;

force.z = 0;

  Then we move the player using are new values we have placed in the temporary force vector. The commented out lines in the code are for A5 and also the latest beta at the time this was published was still having some issues with c_move. So if you have a move error or problem, comment out c_move() and uncomment the A5 movement code. I went ahead and used ent_move() for now.

c_move(my, force.x, nullvector, glide + ignore_passable);

  Next, we make sure the entity is on the floor.

if(MY.FLOOR_DIST > (MY.MAX_Z - MY.MIN_Z)/2)

{

MY.Z -= MY.FLOOR_DIST - (MY.MAX_Z - MY.MIN_Z)/2;

}

  The only thing left now for the movement is to add the new skills and variables that were included in the script. So in Declarations under our Skill Definitions add the following.

 

define prev_anim_state, skill14; // previous animation state for blending

define weapon, skill15; // weapon ID this ent wields

 

  Also add this declaration under Game Variables.

 

var profession_not_set = TRUE; // don't start game until profession set

var force[3]; // temp forces

 

  Now we can give it a test run. Save the script from SED and run it. You should be able to move the player's around now something like this.

 

Adding a Simple Chase Camera

  I think we are getting tired of the static view we have, so now we will add some code for a 3rd person chase camera. Once again, this could be a tutorial in itself so I am not going to explain the camera script. Wwat the camera does however, is follows the player above and behind the player. If a player's back is close to a wall the camera will tilt down as much as possible to keep the player in view and also keep the playing field in view.

  So add this function under function move_player.

 

 

wait(1);

}

}

 

//--------------------------------------------------------------------

// MOVE CAMERA - Simple 3rd Person Chase camera

//--------------------------------------------------------------------

function move_camera()

{

 

var distance;

var adj_cam_tilt;

var camera_next[3];

 

restart_camera:

 

// wait for player to be created

while (player == NULL)

{

wait (1);

}

 

while (player != NULL) // player exist?

{

camera_next.x = player.x - cos (player.pan) * 200;

camera_next.y = player.y - sin (player.pan) * 200;

camera_next.z = player.z + 5;

 

you = player;

 

// make sure camera has unobstructed view

trace_mode = IGNORE_SPRITES + IGNORE_YOU + IGNORE_PASSABLE + USE_BOX;

distance = TRACE(player.x,camera_next.x);

if (distance)

{

distance -= 2;

camera.x = player.x - cos (player.pan) * distance;

camera.y = player.y - sin (player.pan) * distance;

}

else

{

camera.x = player.x - cos (player.pan) * 200;

camera.y = player.y - sin (player.pan) * 200;

}

 

camera.z = player.z + 75;

camera.pan = player.pan;

if(distance==0)

{

distance = 200;

}

 

// if players back close to wall, tilt camera down some so we can see

// player as long as possible and still be able to see

// play field

 

adj_cam_tilt = (100/distance);

 

if(adj_cam_tilt > 1.75)

{

adj_cam_tilt = 1.75;

}

 

camera.tilt = -10;

camera.tilt += (camera.tilt*adj_cam_tilt);

adj_cam_tilt = 0;

camera.roll = 0;

 

wait (1);

}

 

goto(restart_camera);

}

 

  Now let's add the call to the move_camera() function from main() like so.

 

create_player(); // create player

move_camera(); // call simple 3rd Person chase camera

 

  Go ahead and save from SED and run the Host. You will now have a totally different perception of the level like this.

 

  Go ahead and Publish or Resource the game from WED now and copy multiplayer.cd over to your secondary computer and run the Host and then the Client and the program should actually start looking like a real game like this.

  That seems like a good place to end this chapter. We now have our players moving about which should bring great joy to us all. If you get bored and want to have some big excitement, since we don't have any player disconnection code for the entities yet, you can keep quitting and starting the Client creating a massive number of player entities even though there will only be 2 connections. The camera code on the client may be a bit jerky, this I have found from some test is due to something with the current A6.30 dplay_smooth code which is set to 2 as default. Let's go ahead and shut it off before I forget. Add this code to declarations. This problem may be fixed in the future, but for now we are shutting it down.

 

 // Engine Variable

var fps_max = 60; // lock fps at 60 for all players

var fps_lock = ON;

var dplay_smooth = 0; // dplay_smooth is causing too much overshoot and jerks

 

  Now we are done with this chapter.

 

Previous Page Contents Next Page