[EDIT]I could have sworn I clicked [GDK]... How do I change it?[/EDIT]
A fellow user has inquired about some
2D physics in action. Particularly regarding 2D Trajectory of shooting projectiles.
This snippet declares a Projectile class that handles Gravity and Wind effects during its traversal through space.
When running, click (and hold) the Left mouse to identify a starting point - marked by an "X", and drag the mouse. The position of the mouse when you *release* the mouse click determines the direction and velocity of the projectile.
Any angle between 0.000001f and 179.999999f will have gravity impact the shot in a negative manner (to help reflect boyancy(?)) of the projectile; whereas an angle between 180.0f and 360.0f will have gravity impact the shot in a positive (read: normal) manner.
/*
An example snippet showing a 2D Trajectory path for a projectile in flight.
Mouse Controls:
* Click and Hold the left mouse button at a starting <x,y> position.
* Drag the mouse: Up, Down, Left and Right.
* Release the left mouse button to launch the projectile.
NOTE: The further from the starting <x,y> position you are when releasing
the left mouse button, the higher the velocity of the launched
projectile will be. Furthermore, the angle between the starting
<x,y> position and the point where you release the left mouse button
will be negated to aid in the trajectory calculations used to propel
the projectile forward.
*/
#include "DarkGDK.h"
#define TICK_TIME 25 // AI will be given time to "think" every 25ms (40 times per second)...
#define MAX_TICKS 10 // AI will be allowed to make up a maximum of 10 "think" cycles at a time...
#define STR_LENGTH 100 // Maximum string length for status messages on screen...
// Used when displaying lines between points...
#define LINE_OFFSET_X 4.0f
#define LINE_OFFSET_Y 7.0f
#define MAX_VELOCITY 75.0f // Maximum Velocity we'll allow
#define MAX_VELOCITY_SQR 5625.0f // MAX_VELOCITY * MAX_VELOCITY
#define GRAVITATIONAL_FORCE 9.81f // Gravity pulls down: Near earth forces...
int Random_Range(const int lo, const int hi)
{
// For some reason, dbRND() is not *random* enough. As far as I can
// tell, it's because dbRND() takes the *expected* result ranges into
// account by pre-dividing the result of rand() by 10/100/1000 etc.
// (see source code for dbRND() for a better understanding of what I'm
// referring to). For this reason, we will bypass dbRND() in favor of
// the standard rand() function result - without the pre-division...!
return lo + (rand() % ((hi-lo)+1));
}
struct POINT_FLT {
float x;
float y;
};
class Projectile {
protected:
bool m_ShotInProgress;
POINT_FLT m_ShotStartPosition;
POINT_FLT m_ShootThroughPos;
POINT_FLT m_PositionOfShot;
POINT_FLT m_VelocityVector;
POINT_FLT m_DirectionVector;
float m_AngleOfLaunch;
float m_Velocity;
float m_WindSpeed;
float m_TotalElapsed;
float m_Gravity;
void Clear(void) {
m_ShotInProgress=false;
m_ShotStartPosition.x=m_ShotStartPosition.y=0.0f;
m_ShootThroughPos.x=m_ShootThroughPos.y=0.0f;
m_PositionOfShot.x=m_PositionOfShot.y=0.0f;
m_VelocityVector.x=m_VelocityVector.y=0.0f;
m_DirectionVector.x=m_DirectionVector.y=0.0f;
m_AngleOfLaunch=m_Velocity=0.0f;
m_WindSpeed=m_TotalElapsed=0.0f;
m_Gravity=GRAVITATIONAL_FORCE;
}
public:
Projectile(void) { Reset(); }
~Projectile(void) { Clear(); }
bool ShotInProgress(void) const { return m_ShotInProgress; }
float StartPositionX(void) const { return m_ShotStartPosition.x; }
float StartPositionY(void) const { return m_ShotStartPosition.y; }
float ShootThroughPosX(void) const { return m_ShootThroughPos.x; }
float ShootThroughPosY(void) const { return m_ShootThroughPos.y; }
float DirectionVectorX(void) const { return m_DirectionVector.x; }
float DirectionVectorY(void) const { return m_DirectionVector.y; }
float AngleOfLaunch(void) const { return m_AngleOfLaunch; }
float Velocity(void) const { return m_Velocity; }
void Reset(void) {
m_ShotInProgress=false;
m_ShotStartPosition.x=m_ShotStartPosition.y=-1.0f;
m_ShootThroughPos.x=m_ShootThroughPos.y=-1.0f;
m_PositionOfShot.x=m_PositionOfShot.y=-1.0f;
m_VelocityVector.x=m_VelocityVector.y=0.0f;
m_DirectionVector.x=m_DirectionVector.y=0.0f;
m_AngleOfLaunch=m_Velocity=0.0f;
m_WindSpeed=static_cast<float>(Random_Range(-5,5));
m_TotalElapsed=0.0f;
m_Gravity=GRAVITATIONAL_FORCE;
}
void SetStartPoint(const int x, const int y) {
if (!m_ShotInProgress) {
m_ShotStartPosition.x=static_cast<float>(x);
m_ShotStartPosition.y=static_cast<float>(y);
}
}
void ProjectThroughPoint(const int through_x, const int through_y) {
if (!m_ShotInProgress) {
m_ShootThroughPos.x=static_cast<float>(through_x);
m_ShootThroughPos.y=static_cast<float>(through_y);
}
}
void Render(void) const {
if (!m_ShotInProgress)
{
if ((m_ShotStartPosition.x==-1.0f) || (m_ShotStartPosition.y==-1.0f))
return;
dbText(static_cast<int>(m_ShotStartPosition.x),
static_cast<int>(m_ShotStartPosition.y), "X");
if ((m_ShootThroughPos.x==-1.0f) || (m_ShootThroughPos.y==-1.0f))
return;
dbText(static_cast<int>(m_ShootThroughPos.x),
static_cast<int>(m_ShootThroughPos.y), "*");
dbLine(static_cast<int>(m_ShootThroughPos.x+LINE_OFFSET_X),
static_cast<int>(m_ShootThroughPos.y+LINE_OFFSET_Y),
static_cast<int>(m_ShotStartPosition.x+LINE_OFFSET_X),
static_cast<int>(m_ShotStartPosition.y+LINE_OFFSET_Y));
}
else
{
dbText(static_cast<int>(m_PositionOfShot.x+LINE_OFFSET_X),
static_cast<int>(m_PositionOfShot.y+LINE_OFFSET_Y), "*");
}
}
void LaunchShot(const int through_x, const int through_y) {
if (m_ShotInProgress)
return;
m_ShootThroughPos.x=m_PositionOfShot.x=static_cast<float>(through_x);
m_ShootThroughPos.y=m_PositionOfShot.y=static_cast<float>(through_y);
m_DirectionVector.x=m_ShootThroughPos.x-m_ShotStartPosition.x;
m_DirectionVector.y=m_ShootThroughPos.y-m_ShotStartPosition.y;
m_AngleOfLaunch=dbWrapValue(dbAtanFull(m_DirectionVector.x, m_DirectionVector.y)-90);
float fast_distance=(m_DirectionVector.x*m_DirectionVector.x)+
(m_DirectionVector.y*m_DirectionVector.y);
// If our velocity is to low, abort now...
if (fast_distance<9.0f)
{
Reset();
return;
}
// If our velocity is to high, cap it off...
if (fast_distance>MAX_VELOCITY_SQR)
fast_distance=MAX_VELOCITY_SQR;
// We will use the distance between m_ShotStartPostion and
// m_ShootThroughPos as our projectile's velocity...
m_Velocity=sqrt(fast_distance);
// Velocity adjustments per m/s of elapsed *real* time...
m_VelocityVector.x=(m_Velocity*dbCos(m_AngleOfLaunch));
m_VelocityVector.y=(m_Velocity*dbSin(m_AngleOfLaunch));
if ((m_AngleOfLaunch>180.0f) && (m_AngleOfLaunch<360.0f))
m_Gravity=-(m_Gravity);
m_ShotInProgress=true;
}
void PlotShot(const int elapsed) {
if (!m_ShotInProgress)
return;
m_TotalElapsed+=static_cast<float>(elapsed)*0.001f;
float elapsed_squared=static_cast<float>(m_TotalElapsed*m_TotalElapsed);
float wind_adjust=(m_WindSpeed/5.0f);
m_PositionOfShot.x=m_ShotStartPosition.x+
(1.0f * (m_VelocityVector.x * static_cast<float>(m_TotalElapsed))+
(0.5f * wind_adjust * elapsed_squared));
m_PositionOfShot.y=m_ShotStartPosition.y+
((-1.0f * (m_VelocityVector.y * static_cast<float>(m_TotalElapsed))+
(0.5f * m_Gravity * elapsed_squared)));
// If we're not showing anymore, so reset it for next time...
if ((m_PositionOfShot.x<0.0f) || (m_PositionOfShot.y<0.0f) ||
(m_PositionOfShot.x>static_cast<float>(dbScreenWidth())) ||
(m_PositionOfShot.y>static_cast<float>(dbScreenHeight())))
{
Reset();
}
}
};
void DarkGDK(void)
{
dbSyncOn();
dbSyncRate(0); // We want to allow screen updates to happen as quickly as possible...
// Default settings of 800x600x32 bit color depth...
int disp_width=800, disp_height=600, disp_depth=32;
// To override GDK's default refresh rate of 60-fps, we need to ignore VSync settings...
dbSetDisplayModeVSync(disp_width, disp_height, disp_depth, 0);
int tick_start, tick_end; // Timer values used to determine AI "think" cycles...
int num_ticks, frame_time; // How many AI "think" cycles were handled and time required...
char fps_string[STR_LENGTH]; // Used to show screen FPS rates...
bool can_render=true; // Always true for this snippet...
bool is_networked=false; // Always false for this snippet...
char instructions[STR_LENGTH]; // Instruction text to show the user...
char angle_info[STR_LENGTH]; // Informative text to show the user...
bool left_click_down=false; // Previous state of the L-MOUSE Button...
Projectile projectile;
tick_start=dbTimer();
while (LoopGDK())
{
num_ticks=frame_time=0;
tick_end=dbTimer();
// Is it time for the AI to "think"...? If so then let it do so...
while (((tick_end-tick_start)>TICK_TIME) && (num_ticks<MAX_TICKS))
{
projectile.PlotShot(TICK_TIME); // projectile instance needs to think...
tick_start+=TICK_TIME;
frame_time+=TICK_TIME;
++num_ticks;
}
// If we're not running a networked game and we've had an unprocessed
// AI "think" cycle pass by, it's probably due to either the player
// entering a "PAUSE" state (which this snippet does not handle) or
// the player has ALT+TAB'd to another program (or some other such
// situation). In either case, we need to ignore the fact that the
// TICK_TIME has elapsed. However, if we're running a networked game,
// we could be suffering from network "lag" and so we really can't
// ignore the fact that TICK_TIME has elapsed - which is why we have
// the "num_ticks" conditional check in the code above...
if ((!is_networked) && ((tick_end-tick_start) > TICK_TIME))
tick_start=(tick_end-TICK_TIME);
//*************************************************************************
// Handle user input and other between screen refresh processing here...
//*************************************************************************
if (dbMouseClick()==2) {
projectile.Reset();
} else if (dbMouseClick()==1) {
if (!left_click_down)
{
projectile.SetStartPoint(dbMouseX(),dbMouseY());
left_click_down=true;
}
else
{
projectile.ProjectThroughPoint(dbMouseX(),dbMouseY());
}
} else {
// No Mouse-Clicks are detected now, but the Left-Mouse-Click
// was previously recognized; therefore, if the shot was not in progress
// at our last check, it is being launched now...
if ((left_click_down) && (!projectile.ShotInProgress()))
{
projectile.LaunchShot(dbMouseX(),dbMouseY());
}
left_click_down=false;
}
//*************************************************************************
// Update the current state text information for visual display below...
//*************************************************************************
sprintf_s(fps_string, STR_LENGTH, "FPS: %4d", dbScreenFPS());
sprintf_s(instructions, STR_LENGTH,
"Start: <%3d,%3d> Shoot Through: <%3d,%3d> Angle: %3.2f Velocity: %3.2f",
static_cast<int>(projectile.StartPositionX()),
static_cast<int>(projectile.StartPositionY()),
static_cast<int>(projectile.ShootThroughPosX()),
static_cast<int>(projectile.ShootThroughPosY()),
projectile.AngleOfLaunch(),
projectile.Velocity());
sprintf_s(angle_info, STR_LENGTH, "ATAN2(%8.4f,%8.4f) -> %8.4f",
projectile.DirectionVectorY(),
projectile.DirectionVectorX(),
projectile.AngleOfLaunch());
//*************************************************************************
// Normally, can_render will be determined by a call to dbScreenInvalid()
// and related code. We do not handle such functionality here for this
// snippet...
//*************************************************************************
if (can_render)
{
dbCLS();
projectile.Render();
dbText(0,0,instructions);
dbText(0,10,angle_info);
dbText(0,20,fps_string);
dbSync();
}
}
return;
}
I know it's a long snippet of code, but I believe it's very well commented and (hopefully) it will help others too...
Regards,
JTK
Edit: Fixed the href link...