
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 3We 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 bethe point around which it is rotated. It will rotate around the Z axis by default. You cancheck 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 accelerationvalues 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, |