Quake DeveLS - Adding acceleration into func_rotating

Author: Statler
Difficulty: Easy

Adding acceleration into func_rotating:

Adding acceleration into the func_rotating entity may seem a daunting task when looking at the code for accelerative movement with platforms, but it is actually a lot easier to understand.

Fortunately, the edict_t struct already has accel and decel fields, so we have everything we need to make it work. We will need to do the following things:

To set things up, let us first look at the base func_rotating code to understand how it works.

If you'll look at the SP_func_rotating function, you'll notice that apart from the usual type setup stuff for an entity (checking spawnflags, making sure some base values are set), all it does is set the use function, and call gi.setmodel() and gi.linkentity().

So let's take a look at the use function and see if we can tell where the actual rotation is taking place:

void rotating_use (edict_t *self, edict_t *other, edict_t *activator)
{
   if (!VectorCompare (self->avelocity, vec3_origin))
   {
      self->s.sound = 0;
      VectorClear (self->avelocity);
      self->touch = NULL;
   }
   else
   {
      self->s.sound = self->moveinfo.sound_middle;
      VectorScale (self->movedir, self->speed, self->avelocity);
      if (self->spawnflags & 16)
         self->touch = rotating_touch;
   }
}

What we have here is a simple check to see if the entity is currently rotating. If the entity's avelocity is not equal to vec3_origin (which is the vector {0, 0, 0}, then turn off the sound, clear the angular velocity, and turn off its touch function.

If the entities angular velocity is equal to all zeros (i.e. it currently has no angular velocity) then give it a sound, call VectorScale to set the avelocity vector up correctly, and turn on its touch function if need be.

Not complicated at all eh.. so understanding that, all we need to do to add acceleration is to bring a think function into play for the entity that follows this logic:

If the entity is currently stopped or decelerating, then begin accelerating up to full speed. If the entity is currently accelerating or at full speed, then begin decelerating down to full stop.

To reach this end, we shall setup two think functions, Think_RotateAccel and Think_RotateDecel.

First of all, the easiest way to ensure that the logic is enforced correctly is to bring a state machine into play, and for this we want to setup some state variables as #include lines at the top of the g_func.c file. So if you haven't already, load up g_func.c and lets get to coding...

Add the following lines up at the top with the other #include lines: (a + indicates added lines)

#define	STATE_TOP      0
#define	STATE_BOTTOM   1
#define STATE_UP       2
#define STATE_DOWN     3

+ #define STATE_STOPPED     0
+ #define STATE_ACCEL       1
+ #define STATE_FULLSPEED   2
+ #define STATE_DECEL       3

We will use these later on to keep track of what state the entity is currently in. Now drop down to the SP_func_rotating declaration and modify it thusly:

/*QUAKED func_rotating (0 .5 .8) ? START_ON REVERSE X_AXIS Y_AXIS TOUCH_PAIN STOP ANIMATED ANIMATED_FAST
You need to have an origin brush as part of this entity.  The center of that brush will be
the point around which it is rotated. It will rotate around the Z axis by default.  You can
check either the X_AXIS or Y_AXIS box to change that.

"speed" determines how fast it moves; default value is 100.
"dmg"	damage to inflict when blocked (2 default)
"accel" acceleration speed when activated, goes from 0 to speed
"decel" deceleration speed when deactivated, goes from speed to 0

Good values for acceleration run from 20-100.  Do not use acceleration
values that are more than 10x the speed setting, or it will have no effect.

REVERSE will cause the it to rotate in the opposite direction.
STOP mean it will stop moving instead of pushing entities
*/

void SP_func_rotating (edict_t *ent)
{
   ent->solid = SOLID_BSP;
   if (ent->spawnflags & 32)
      ent->movetype = MOVETYPE_STOP;
   else
      ent->movetype = MOVETYPE_PUSH;

+  ent->moveinfo.state = STATE_STOPPED; // rotating thingy starts out idle
   // set the axis of rotation
   VectorClear(ent->movedir);
   if (ent->spawnflags & 4)
      ent->movedir[2] = 1.0;
   else if (ent->spawnflags & 8)
      ent->movedir[0] = 1.0;
   else // Z_AXIS
      ent->movedir[1] = 1.0;
   // check for reverse rotation
   if (ent->spawnflags & 2)
      VectorNegate (ent->movedir, ent->movedir);

   if (!ent->speed)
      ent->speed = 100;
   if (!ent->dmg)
      ent->dmg = 2;

+  if (ent->accel < 0) /* sanity check */
+     ent->accel = 0;
+  else
+     ent->accel *= 0.1;
+
+  if (ent->decel < 0) /* sanity check */
+     ent->decel = 0;
+  else
+     ent->decel *= 0.1;
+
+  ent->moveinfo.current_speed = 0;

// ent->moveinfo.sound_middle = "doors/hydro1.wav";

   ent->use = rotating_use;

   if (ent->dmg)
   ent->blocked = rotating_blocked;

   if (ent->spawnflags & 1)
      ent->use (ent, NULL, NULL);

   if (ent->spawnflags & 64)
      ent->s.effects |= EF_ANIM_ALL;
   if (ent->spawnflags & 128)
      ent->s.effects |= EF_ANIM_ALLFAST;

   gi.setmodel (ent, ent->model);
   gi.linkentity (ent);
}

Not much new for that function, just some code to make sure the intial values for accel and decel are set properly. Make sure you add the line near the top that sets the initial motion state of the entity (STATE_STOPPED). Also, notice that the accel and decel values are multiplied by 0.1. Because of how we will be using the values, lower numbers will work better. This way, the accel and decel values look similar to the speed values to the level designer and I think are a bit easier to understand. (also, it was done this way with plats, heh)

Alright, next is the use function, and it gets pretty mangled, so let's just write it again from scratch:

void rotating_use (edict_t *self, edict_t *other, edict_t *activator)
{
   /* first, figure out what state we are in */
   if (self->moveinfo.state == STATE_ACCEL || self->moveinfo.state == STATE_FULLSPEED)
   {
      /* if decel is 0 then just stop */
      if (self->decel == 0) {
         VectorClear(self->avelocity);
         self->moveinfo.current_speed = 0;
         self->touch = NULL;
         self->think = NULL;
         self->moveinfo.state = STATE_STOPPED;
      } else { /* otherwise decelerate */
         self->think = Think_RotateDecel;
         self->nextthink = level.time + FRAMETIME;
         self->moveinfo.state = STATE_DECEL;
      } /* decelerate */
     /* setup touch function if needed */
     if (self->spawnflags & 16)
         self->touch = rotating_touch;
   }
   else
   {
      self->s.sound = self->moveinfo.sound_middle;
      /* check if accel is 0.  If so, just start the rotation */
      if (self->accel == 0) {
         VectorScale (self->movedir, self->speed, self->avelocity);
         self->moveinfo.state = STATE_FULLSPEED;
      } 
      else { /* accelerate baybee */
         self->think = Think_RotateAccel;
         self->nextthink = level.time + FRAMETIME;
         self->moveinfo.state = STATE_ACCEL;
		
      /* setup touch function if needed */
      if (self->spawnflags & 16)
         self->touch = rotating_touch;
   }

}

The above code determines the current state of the machine. If it it currently accelerating or at full speed, then it checks the decel value. If it is zero, then there is no need to decelerate so it calls the original lines to stop it instantly and changes the state to STATE_STOPPED. Otherwise the state is changed into STATE_DECEL and the proper think function is set. If the entity is currently decelerating or stopped, similar logic is used.

We are almost done now. All that remains are the two think functions: Think_RotateAccel and Think_RotateDecel. The basics of the acceleration is this: the direction vector is scaled by the speed to get the angular velocity in the original entity. To simulate acceleration, we can use the accel value to increment the current speed from stopped to the speed value using the think functions. Place the code for the think functions up above the other func_rotating functions.

And here they are:

void Think_RotateAccel (edict_t *self)
{
   if (self->moveinfo.current_speed >= self->speed) { /* has reached full speed*/
      /* if calculation causes it to go a little over, readjust */
      if (self->moveinfo.current_speed != self->speed) 
         VectorScale (self->movedir, self->speed, self->avelocity);
      self->think = NULL;
      self->moveinfo.state = STATE_FULLSPEED;
      return;
   } /* has reached full speed */
   /* if here, some more acceleration needs to be done */
   /* add acceleration value to current speed to cause accel */
   self->moveinfo.current_speed += self->accel;
   VectorScale (self->movedir, self->moveinfo.current_speed, self->avelocity);
   self->nextthink = level.time + FRAMETIME;

} /* Think_RotateAccel */

void Think_RotateDecel (edict_t *self)
{
   if (self->moveinfo.current_speed <= 0) { /* has reached full speed*/
      /* if calculation cause it to go a little under, readjust */
      if (self->moveinfo.current_speed != 0)
      {
         VectorClear (self->avelocity);
         self->moveinfo.current_speed = 0;
      }
      self->think = NULL;
      self->moveinfo.state = STATE_STOPPED;
      return;
   } /* has reached full stop */
   /* if here, some more deceleration needs to be done */
   /* subtract deceleration value from current speed to cause decel */
   self->moveinfo.current_speed -= self->decel;
   VectorScale (self->movedir, self->moveinfo.current_speed, self->avelocity);
   self->nextthink = level.time + FRAMETIME;

} /* Think_RotateDecel */

I hope things were documented well enough to make sense. Now let's think about an example of how this would work. Let's say you have a level with this freshly redesigned func_rotating entity in it. It has a speed value of 100 and accel and decel values of 100. When the entity is spawned, the state is set to STATE_STOPPED. If the entity is triggered or is set to start on, then the use function is called. Since we are in STATE_STOPPED and accel is != 0, then the think function is setup and will be called at the next frame. The accel function increments the current speed from 0 to full speed (in this case, 100) in increments of the accel value, which we set to 100, but was internally changed to 10. The VectorScale is called each time to set the proper values in the avelocity vector. The think function is recalled every frame until the current_speed has accelerated up to or above the speed value. If it has gone over, it readjusts to speed and calls VectorScale again to get it running at full speed. The state is then changed to STATE_FULLSPEED and the think function is set to null.

Because of how the state machine is setup, you can make a func_rotating that is targetted and it can be toggled while accelerating or decelerating and still adjust properly. Specifically, if you have a button to control a func_rotating, hit the button to start it rotating, and hit the button again before it has reached full speed, it will decelerate from its current speed back down to zero (unless you are infatuated by it and keep hitting the button over and over).

Well I hope you enjoyed this little tutorial. I sure enjoyed writing it (I think). And you thought those finite state machines you learned in class would never come in handy. :) -Statler

Tutorial by Statler


This site, and all content and graphics displayed on it,
are ©opyrighted to the Quake DeveLS team. All rights received.
Got a suggestion? Comment? Question? Hate mail? Send it to us!
Oh yeah, this site is best viewed in 16 Bit or higher, with the resolution on 800*600.
Thanks to Planet Quake for there great help and support with hosting.
Best viewed with Netscape 4 or IE 3