我们的游戏中已经有了一些物理知识。我们的每艘船都有速度和加速度。它们也至少遵守一些牛顿定律并保持动量。所有这些都是在没有大张旗鼓的情况下提前添加的。电脑游戏中的物理可以追溯到最初的电脑游戏太空战!,也就是启发了我们目前正在写的这款游戏。太空战原版中!,宇宙飞船保持动量,就像我们目前在游戏中做的那样。一个黑洞在引力作用下将船只吸引到游戏区的中心。在创作经典游戏 Pong 之前,诺兰·布什内尔创作了一个太空战的街机克隆!,名为电脑空间。计算机空间并没有像 Pong 那样一炮而红,诺兰·布什内尔将游戏商业失败的部分原因归咎于牛顿定律和公众对基础物理的缺乏理解。
According to The Ultimate History of Video Games: from Pong to Pokemon and Beyond, by Steven Kent, "Computer Space obeys the first law—maintenance of momentum. (Bushnell is probably referring to Sir Isaac Newton's first law—objects maintain constant velocity unless acted upon by an external force.) And so that was really hard for people who didn't understand that." – Nolan Bushnell
物理在游戏中很常见,但远非通用。游戏所需的物理种类高度依赖于游戏的种类。有一个名为子弹物理的 3D 物理库已经被移植,但是,因为它是 3D 的,子弹对于我们将在这个游戏中使用的物理种类来说是一个相当大的库。相反,我们将把一些简单的牛顿物理整合到我们的游戏中,以获得一些额外的味道。我们已经在游戏中简单实现了牛顿第一定律。当我们加速我们的宇宙飞船时,它向同一个方向移动,直到我们或者用向下的箭头使它减速,或者我们翻转并燃烧使我们的飞船转向并向与我们当前速度相反的方向加速。
You will need to include several images and audio files in your build to make this project work. Make sure that you include the /Chapter13/sprites/
folder as well as the /Chapter13/audio/
folder from the project's GitHub. If you haven't yet downloaded the GitHub project, you can get it online at https://github.com/PacktPublishing/Hands-On-Game-Development-with-WebAssembly.
在本章中,我们将应用物理学的以下方面:
- 小行星、射弹和宇宙飞船之间的弹性碰撞。
- 当我们的宇宙飞船射击时,应该有后坐力(牛顿第三定律)。
- 来自恒星的引力应该会吸引玩家的飞船。
牛顿第三定律通常被表述为,对于每一个动作,都有一个相等且相反的反作用力。这意味着,当物体 A 对物体 B 施加力时,物体 B 对物体 A 施加同样的力。这方面的一个例子是用枪发射子弹。当一个拿着枪的人发射子弹时,枪的后座力与子弹离开枪的力相同。这听起来可能有悖常理,因为子弹可以杀死人,但枪的后坐力不会杀死开枪的人。那是因为枪明显比子弹大,牛顿第一定律说 F = ma ,或者说力等于质量乘以加速度。换句话说,如果枪比子弹大 50 倍,那么同样的力只会让它加速到 1/50 的速度。我们将修改我们的宇宙飞船,这样,每当它发射一枚射弹时,它就会根据宇宙飞船和射弹的相对质量,向与发射方向相反的方向加速。这会给我们船上的大炮一个后坐力。
在我们给飞船的加农炮增加后坐力之后,我还想在我们的游戏中给宇宙飞船增加一个重力效应,当飞船在恒星的某个距离内时,它会把飞船拉向恒星。引力随着两个物体之间距离的平方而减小。这很方便,因为这意味着我们可以用MagSQ
函数计算重力效应,它比Magnitude
函数运行得快得多。出于个人喜好,我选择不在抛射体和小行星上添加引力效应。如果你选择这样做,就不难增加这种效果。
我们将在游戏中改进我们的宇宙飞船与小行星和抛射体之间的碰撞。为了简化事情,我们将使用弹性碰撞。弹性碰撞是保留所有动能的碰撞。在现实中,碰撞总是会因为热量或摩擦而损失一些能量,即使是那些接近弹性碰撞的碰撞,比如台球。然而,使我们的碰撞完全有弹性简化了数学。在游戏中,更简单的数学通常意味着更快的算法。
关于弹性碰撞的更多信息,维基百科有一篇优秀的文章(https://en . Wikipedia . org/wiki/Elastic _ collection)讨论了我们将用来实现弹性碰撞功能的数学。
在这一节中,我们将对我们的游戏对象进行一些更改。我们需要增加质量和弹性碰撞到我们的collider
类。我们的恒星应该能够产生引力,并以基于距离的平方而减小的力吸引玩家和敌方飞船。我们将需要修改我们的碰撞功能,以增加我们的宇宙飞船、小行星和射弹之间的弹性碰撞。
为了让物理进入我们的游戏,我们需要修改几个类定义并添加新的#define
宏。让我们从更新我们的game.hpp
文件开始。我们首先需要添加的是#define
,以便为我们的恒星质量设置一个恒定值。我希望恒星质量有一个大的常数值,我们将在ElasticCollision
函数中对其进行检查。如果在我们的弹性碰撞中,任何一个物体的质量与STAR_MASS
的质量相同,我们就不想加速那个物体。实际上,如果你把一块石头扔向太阳,你会在你扔石头的方向上,使太阳加速一点点。这个数量相对于太阳来说非常小,以至于无法探测到。我们将有一个恒星质量的固定值,在我们的游戏中,任何质量如此大的物体被任何物体击中时都不会加速。为此,我们需要添加以下#define
:
#define STAR_MASS 9999999
添加#define
后,我们需要修改我们的Collider
类,给它一个新的ElasticCollision
功能。该功能将接收第二个Collider
物体,并使用这两个物体的速度和质量来确定它们的新速度。我们还需要添加一个我们将命名为m_Mass
的质量属性。最后,我们需要将两个属性移到我们的Collider
类中,这个类以前在Collider
的子类中。这些变量是 2D m_Direction
和m_Velocity
向量,因为我们的弹性碰撞函数需要这些数据来计算新的速度。这就是新版Collider
类的样子:
class Collider {
public:
bool m_Active;
float* m_ParentRotation;
float* m_ParentX;
float* m_ParentY;
Vector2D m_TempPoint;
bool CCHitTest( Collider* collider );
void ElasticCollision( Collider* collider );
float m_Mass;
Vector2D m_Direction;
Vector2D m_Velocity;
Vector2D m_Position;
float m_Radius;
float m_SteeringRadius;
float m_SteeringRadiusSQ;
void SetParentInformation( float* rotation, float* x, float* y );
Collider(float radius);
bool HitTest( Collider *collider );
bool SteeringLineTest( Vector2D &p1, Vector2D &p2 );
bool SteeringRectTest( Vector2D &start_point, Vector2D
&end_point );
void WrapPosition();
};
我们添加的四行靠近这个新版本类的中心:
void ElasticCollision( Collider* collider );
float m_Mass;
Vector2D m_Direction;
Vector2D m_Velocity;
在将m_Direction
和m_Velocity
添加到我们的Collider
类之后,我们需要从三个子类中删除m_Velocity
,在我们的游戏的先前版本中,这些子类中有这些代码。我们需要从Asteroid
、Ship
和Projectile
类中移除这些属性。下面是我们需要删除的两行:
Vector2D m_Direction;
Vector2D m_Velocity;
在下面的代码片段中,在您删除了这两行之后,我们有了Asteroid
类:
class Asteroid : public Collider {
public:
SDL_Texture *m_SpriteTexture;
SDL_Rect m_src = {.x = 0, .y = 0, .w = 16, .h = 16 };
SDL_Rect m_dest = {.x = 0, .y = 0, .w = 0, .h = 0 };
Uint32 m_CurrentFrame = 0;
int m_NextFrameTime;
float m_Rotation;
Emitter* m_Explode;
Emitter* m_Chunks;
Asteroid( float x, float y,
float velocity,
float rotation );
void Move();
void Render();
void Explode();
};
这就是删除这两行后Ship
类的样子:
class Ship : public Collider {
public:
const float c_Acceleration = 10.0f;
const float c_MaxVelocity = 100.0f;
const int c_AliveTime = 2000;
const Uint32 c_MinLaunchTime = 300;
bool m_Accelerating = false;
Uint32 m_LastLaunchTime;
const int c_Width = 32;
const int c_Height = 32;
SDL_Texture *m_SpriteTexture;
SDL_Rect src = {.x = 0, .y = 0, .w = 32, .h = 32 };
Emitter* m_Explode;
Emitter* m_Exhaust;
Shield* m_Shield;
std::vector<Collider*> m_Colliders;
Uint32 m_CurrentFrame = 0;
int m_NextFrameTime;
float m_Rotation;
void RotateLeft();
void RotateRight();
void Accelerate();
void Decelerate();
void CapVelocity();
void Shoot();
virtual void Move() = 0;
Ship();
void Render();
bool CompoundHitTest( Collider* collider );
};
最后,下面是删除这两行后Projectile
类的样子:
class Projectile: public Collider {
public:
const char* c_SpriteFile = "sprites/ProjectileExp.png";
const int c_Width = 16;
const int c_Height = 16;
SDL_Texture *m_SpriteTexture;
SDL_Rect src = {.x = 0, .y = 0, .w = 16, .h = 16 };
Uint32 m_CurrentFrame = 0;
int m_NextFrameTime;
const float c_Velocity = 300.0;
const float c_AliveTime = 2000;
float m_TTL;
Projectile();
void Move();
void Render();
void Launch(Vector2D &position, Vector2D &direction);
};
我们必须改变的最后一个阶层是我们的Star
阶层。Star
级现在将能够通过引力吸引我们游戏中的宇宙飞船。为此,我们将添加一个常数属性,定义我们引力的最大范围。在现实中,重力会永远延伸下去,但是对于我们的游戏来说,我们不希望当恒星离屏幕很远(或者至少离屏幕很远)时,重力会影响我们的宇宙飞船。正因为如此,我们将把引力效应的距离限制在 500 像素。我们还将为我们的类添加一个名为ShipGravity
的新函数。我们将传递一个Ship
物体到这个函数中,这个函数将根据到Star
物体的平方距离来修改船的速度。这就是新版Star
类定义的样子:
class Star : public Collider {
public:
const float c_MaxGravityDistSQ = 250000.0; // 300 squared
SDL_Texture *m_SpriteTexture;
SDL_Rect m_src = {.x = 0, .y = 0, .w = 64, .h = 64 };
SDL_Rect m_dest = {.x = 0, .y = 0, .w = 64, .h = 64 };
std::vector<Emitter*> m_FlareList;
Uint32 m_CurrentFrame = 0;
int m_NextFrameTime;
Star();
void Move();
void Render();
void ShipGravity( Ship* s );
};
我们将更改的下一个文件是collider.cpp
文件,它保存了我们在Collider
类定义中声明的函数。唯一的变化是增加了一个单一的功能,ElasticCollision
。这个函数根据物体的质量和起始速度来修改我们两台对撞机的位置和速度。这就是ElasticCollision
功能的样子:
void Collider::ElasticCollision( Collider* collider ) {
if( collider->m_Mass == STAR_MASS || m_Mass == STAR_MASS ) {
return;
}
Vector2D separation_vec = collider->m_Position - m_Position;
separation_vec.Normalize();
separation_vec *= collider->m_Radius + m_Radius;
collider->m_Position = m_Position + separation_vec;
Vector2D old_v1 = m_Velocity;
Vector2D old_v2 = collider->m_Velocity;
m_Velocity = old_v1 * ((m_Mass - collider->m_Mass)/(m_Mass +
collider->m_Mass)) +
old_v2 * ((2 * collider->m_Mass) / (m_Mass + collider->m_Mass));
collider->m_Velocity = old_v1 * ((2 * collider->m_Mass)/(m_Mass +
collider->m_Mass)) +
old_v2 * ((collider->m_Mass - m_Mass)/(m_Mass + collider->m_Mass));
}
这个函数做的第一件事是检查两个对撞机是否都有恒星的质量。如果其中一个是恒星,我们不会改变它们的速度。恒星的速度不会改变,因为它的质量太大而无法移动,与恒星碰撞的物体不会改变质量,因为它在碰撞中被摧毁了:
if( collider->m_Mass == STAR_MASS || m_Mass == STAR_MASS ) {
return;
}
质量检查后,我们需要调整对撞机的位置,使它们不重叠。重叠可能会发生,因为我们的对象的位置每帧都在变化,并且不是连续的。因此,我们需要移动其中一个物体的位置,使其几乎不接触另一个物体。更准确的方法是将两个对象的位置修改为我们修改一个对象的一半,但方向不同。为了简单起见,我们将只改变其中一个碰撞器的位置:
separation_vec.Normalize();
separation_vec *= collider->m_Radius + m_Radius;
collider->m_Position = m_Position + separation_vec;
之后,我们将使用这两个物体的质量和起始速度来修改两个对撞机物体的速度:
Vector2D old_v1 = m_Velocity;
Vector2D old_v2 = collider->m_Velocity;
m_Velocity = old_v1 * ((m_Mass - collider->m_Mass)/(m_Mass + collider->m_Mass)) +
old_v2 * ((2 * collider->m_Mass) / (m_Mass + collider->m_Mass));
collider->m_Velocity = old_v1 * ((2 * collider->m_Mass)/(m_Mass + collider->m_Mass)) +
old_v2 * ((collider->m_Mass - m_Mass)/(m_Mass + collider->m_Mass));
如果你想了解更多关于我们用来计算新速度的公式,请查阅维基百科关于 https://en.wikipedia.org/wiki/Elastic_collision 弹性碰撞的文章。
在我们的star.cpp
文件中,我们将需要修改我们的Star
类的构造函数,以及它的Move
函数。我们还需要添加一个名为ShipGravity
的新功能。我们要做的第一件事是在我们的Star
类构造函数中的某个地方添加以下行:
m_Mass = STAR_MASS;
之后,我们需要定义我们的ShipGravity
函数。以下代码定义了该函数:
void Star::ShipGravity( Ship* s ) {
Vector2D dist_vec = m_Position - s->m_Position;
float dist_sq = dist_vec.MagSQ();
if( dist_sq < c_MaxGravityDistSQ ) {
float force = (c_MaxGravityDistSQ / dist_sq) * delta_time;
dist_vec.Normalize();
dist_vec *= force;
s->m_Velocity += dist_vec;
}
}
第一行创建一个dist_vec
向量,这是一个表示恒星位置和船只位置之间距离的向量。第二条线得到恒星和飞船之间的平方距离。之后,我们有一个if
块,看起来像这样:
if( dist_sq < c_MaxGravityDistSQ ) {
float force = (c_MaxGravityDistSQ / dist_sq) * delta_time;
dist_vec.Normalize();
dist_vec *= force;
s->m_Velocity += dist_vec;
}
这个if
块正在对照重力影响船只的最大距离检查平方距离,我们在c_MaxGravityDistSQ
常数中定义了这个距离。因为引力随着恒星和我们飞船之间距离的平方而减小,所以我们通过将最大引力距离除以到我们飞船的距离平方的 50 倍来计算标量力。50 的值是相当随意选择的,是我玩弄数字直到重力感觉适合我的结果。如果你希望你的重力不同,你可以选择不同的值。您也可以选择通过更改我们在game.hpp
中定义的c_MaxGravityDistSQ
的值来修改最大重力距离。以下几行用于将我们的标量力值转换为从我们的船指向我们的星的矢量力值:
dist_vec.Normalize();
dist_vec *= force;
现在我们已经将dist_vec
转换为指向我们恒星方向的力矢量,我们可以将该力矢量添加到我们飞船的速度中,从而在我们的飞船上产生重力效应:
s->m_Velocity += dist_vec;
我们需要做的最后一个改变是Move
功能。我们需要向ShipGravity
函数添加两个调用;一次召唤对玩家产生引力效果,第二次召唤对敌方飞船产生引力效果。以下是新版本的Move
功能:
void Star::Move() {
m_NextFrameTime -= diff_time;
if( m_NextFrameTime <= 0 ) {
++ m_CurrentFrame;
m_NextFrameTime = ms_per_frame;
if( m_CurrentFrame >= 8 ) {
m_CurrentFrame = 0;
}
}
ShipGravity( player );
ShipGravity( enemy );
}
最后两行是新的。确保将这两行添加到Move
功能中:
ShipGravity( player );
ShipGravity( enemy );
在更新我们的star.cpp
文件之后,我们需要改变main.cpp
文件来合并我们的弹性碰撞。我们需要对collisions()
功能进行所有这些更改。以下是collisions
的完整新版本:
void collisions() {
Asteroid* asteroid;
std::vector<Asteroid*>::iterator ita;
if( player->m_CurrentFrame == 0 && player->CompoundHitTest( star ) ) {
player->m_CurrentFrame = 1;
player->m_NextFrameTime = ms_per_frame;
player->m_Explode->Run();
large_explosion_snd->Play();
}
if( enemy->m_CurrentFrame == 0 && enemy->CompoundHitTest( star ) ) {
enemy->m_CurrentFrame = 1;
enemy->m_NextFrameTime = ms_per_frame;
enemy->m_Explode->Run();
large_explosion_snd->Play();
}
Projectile* projectile;
std::vector<Projectile*>::iterator it;
for(it=projectile_pool->m_ProjectileList.begin();
it!=projectile_pool->m_ProjectileList.end();
it++) {
projectile = *it;
if( projectile->m_CurrentFrame == 0 && projectile->m_Active ) {
for( ita = asteroid_list.begin(); ita != asteroid_list.end();
ita++
) {
asteroid = *ita;
if( asteroid->m_Active ) {
if( asteroid->HitTest( projectile ) ) {
asteroid->ElasticCollision( projectile );
projectile->m_CurrentFrame = 1;
projectile->m_NextFrameTime = ms_per_frame;
small_explosion_snd->Play();
}
}
}
if( projectile->HitTest( star ) ){
projectile->m_CurrentFrame = 1;
projectile->m_NextFrameTime = ms_per_frame;
small_explosion_snd->Play();
}
else if( player->m_CurrentFrame == 0 && ( projectile->HitTest(
player ) ||
player->CompoundHitTest( projectile ) ) ) {
if( player->m_Shield->m_Active == false ) {
player->m_CurrentFrame = 1;
player->m_NextFrameTime = ms_per_frame;
player->m_Explode->Run();
large_explosion_snd->Play();
}
else {
hit_snd->Play();
player->ElasticCollision( projectile );
}
projectile->m_CurrentFrame = 1;
projectile->m_NextFrameTime = ms_per_frame;
}
else if( enemy->m_CurrentFrame == 0 && ( projectile-
>HitTest( enemy ) || enemy->CompoundHitTest( projectile ) )
) {
if( enemy->m_Shield->m_Active == false ) {
enemy->m_CurrentFrame = 1;
enemy->m_NextFrameTime = ms_per_frame;
enemy->m_Explode->Run();
large_explosion_snd->Play();
}
else {
enemy->ElasticCollision( projectile );
hit_snd->Play();
}
projectile->m_CurrentFrame = 1;
projectile->m_NextFrameTime = ms_per_frame;
}
}
}
for( ita = asteroid_list.begin(); ita != asteroid_list.end(); ita++ ) {
asteroid = *ita;
if( asteroid->m_Active ) {
if( asteroid->HitTest( star ) ) {
asteroid->Explode();
small_explosion_snd->Play();
}
}
else { continue; }
if( player->m_CurrentFrame == 0 && asteroid->m_Active &&
( asteroid->HitTest( player ) || player->CompoundHitTest(
asteroid ) ) ) {
if( player->m_Shield->m_Active == false ) {
player->m_CurrentFrame = 1;
player->m_NextFrameTime = ms_per_frame;
player->m_Explode->Run();
large_explosion_snd->Play();
}
else {
player->ElasticCollision( asteroid );
small_explosion_snd->Play();
}
}
if( enemy->m_CurrentFrame == 0 && asteroid->m_Active &&
( asteroid->HitTest( enemy ) || enemy->CompoundHitTest(
asteroid ) ) ) {
if( enemy->m_Shield->m_Active == false ) {
enemy->m_CurrentFrame = 1;
enemy->m_NextFrameTime = ms_per_frame;
enemy->m_Explode->Run();
large_explosion_snd->Play();
}
else {
enemy->ElasticCollision( asteroid );
small_explosion_snd->Play();
}
}
}
Asteroid* asteroid_1;
Asteroid* asteroid_2;
std::vector<Asteroid*>::iterator ita_1;
std::vector<Asteroid*>::iterator ita_2;
for( ita_1 = asteroid_list.begin(); ita_1 != asteroid_list.end();
ita_1++ ) {
asteroid_1 = *ita_1;
if( !asteroid_1->m_Active ) { continue; }
for( ita_2 = ita_1+1; ita_2 != asteroid_list.end(); ita_2++ ) {
asteroid_2 = *ita_2;
if( !asteroid_2->m_Active ) { continue; }
if( asteroid_1->HitTest( asteroid_2 ) ) {
asteroid_1->ElasticCollision( asteroid_2 );
}
}
}
}
在这个功能的第一部分,我们在射弹上循环,检查它们是否击中了小行星或船只。如果炮弹击中一颗小行星或一艘船,当那艘船的防护罩打开时,我们想与炮弹产生弹性碰撞。抛射体仍将被摧毁,但飞船或小行星将根据碰撞具有修正的速度。以下是projectile
循环的代码:
for( it = projectile_pool->m_ProjectileList.begin(); it != projectile_pool->m_ProjectileList.end(); it++ ) {
projectile = *it;
if( projectile->m_CurrentFrame == 0 && projectile->m_Active ) {
for( ita = asteroid_list.begin(); ita != asteroid_list.end();
ita++ ) {
asteroid = *ita;
if( asteroid->m_Active ) {
if( asteroid->HitTest( projectile ) ) {
asteroid->ElasticCollision( projectile );
projectile->m_CurrentFrame = 1;
projectile->m_NextFrameTime = ms_per_frame;
small_explosion_snd->Play();
}
}
}
if( projectile->HitTest( star ) ){
projectile->m_CurrentFrame = 1;
projectile->m_NextFrameTime = ms_per_frame;
small_explosion_snd->Play();
}
else if( player->m_CurrentFrame == 0 &&
( projectile->HitTest( player ) ||
player->CompoundHitTest( projectile ) ) ) {
if( player->m_Shield->m_Active == false ) {
player->m_CurrentFrame = 1;
player->m_NextFrameTime = ms_per_frame;
player->m_Explode->Run();
large_explosion_snd->Play();
}
else {
hit_snd->Play();
player->ElasticCollision( projectile );
}
projectile->m_CurrentFrame = 1;
projectile->m_NextFrameTime = ms_per_frame;
}
else if( enemy->m_CurrentFrame == 0 &&
( projectile->HitTest( enemy ) ||
enemy->CompoundHitTest( projectile ) ) ) {
if( enemy->m_Shield->m_Active == false ) {
enemy->m_CurrentFrame = 1;
enemy->m_NextFrameTime = ms_per_frame;
enemy->m_Explode->Run();
large_explosion_snd->Play();
}
else {
enemy->ElasticCollision( projectile );
hit_snd->Play();
}
projectile->m_CurrentFrame = 1;
projectile->m_NextFrameTime = ms_per_frame;
}
}
}
这个循环执行的第一系列检查是针对每一颗小行星。它寻找一颗正在碰撞的活跃的小行星。如果这些条件是真的,它做的第一件事就是调用小行星上的ElasticCollision
函数,传入射弹:
for( ita = asteroid_list.begin(); ita != asteroid_list.end(); ita++ ) {
asteroid = *ita;
if( asteroid->m_Active ) {
if( asteroid->HitTest( projectile ) ) {
asteroid->ElasticCollision( projectile );
projectile->m_CurrentFrame = 1;
projectile->m_NextFrameTime = ms_per_frame;
small_explosion_snd->Play();
}
}
该代码与早期版本相同,但增加了对ElasticCollision
的调用:
asteroid->ElasticCollision( projectile );
稍后,在我们遍历每个活动射弹的循环中,如果一个射弹击中了玩家的飞船,同时它的护盾打开,我们将添加对ElasticCollision
功能的调用:
else if( player->m_CurrentFrame == 0 &&
( projectile->HitTest( player ) ||
player->CompoundHitTest( projectile ) ) ) {
if( player->m_Shield->m_Active == false ) {
player->m_CurrentFrame = 1;
player->m_NextFrameTime = ms_per_frame;
player->m_Explode->Run();
large_explosion_snd->Play();
}
else {
hit_snd->Play();
player->ElasticCollision( projectile );
}
projectile->m_CurrentFrame = 1;
projectile->m_NextFrameTime = ms_per_frame;
}
我们将对一艘被炮弹击中的敌方宇宙飞船做同样的处理:
else if( enemy->m_CurrentFrame == 0 &&
( projectile->HitTest( enemy ) ||
enemy->CompoundHitTest( projectile ) ) ) {
if( enemy->m_Shield->m_Active == false ) {
enemy->m_CurrentFrame = 1;
enemy->m_NextFrameTime = ms_per_frame;
enemy->m_Explode->Run();
large_explosion_snd->Play();
}
else {
enemy->ElasticCollision( projectile );
hit_snd->Play();
}
projectile->m_CurrentFrame = 1;
projectile->m_NextFrameTime = ms_per_frame;
}
}
在循环所有活动的射弹之后,collisions
功能循环所有的小行星,寻找小行星和其中一艘飞船之间的碰撞。如果飞船没有启动护盾,飞船就会被摧毁。我们不对这部分代码进行任何修改。在我们代码的早期版本中,如果飞船确实有护盾,我们就摧毁了小行星。现在,我们将发生弹性碰撞,这将导致宇宙飞船和小行星相互反弹。这就是这个asteroid
循环的样子:
for( ita = asteroid_list.begin(); ita != asteroid_list.end(); ita++ ) {
asteroid = *ita;
if( asteroid->m_Active ) {
if( asteroid->HitTest( star ) ) {
asteroid->Explode();
small_explosion_snd->Play();
}
}
else {
continue;
}
if( player->m_CurrentFrame == 0 &&
asteroid->m_Active &&
( asteroid->HitTest( player ) ||
player->CompoundHitTest( asteroid ) ) ) {
if( player->m_Shield->m_Active == false ) {
player->m_CurrentFrame = 1;
player->m_NextFrameTime = ms_per_frame;
player->m_Explode->Run();
large_explosion_snd->Play();
}
else {
player->ElasticCollision( asteroid );
small_explosion_snd->Play();
}
}
if( enemy->m_CurrentFrame == 0 &&
asteroid->m_Active &&
( asteroid->HitTest( enemy ) ||
enemy->CompoundHitTest( asteroid ) ) ) {
if( enemy->m_Shield->m_Active == false ) {
enemy->m_CurrentFrame = 1;
enemy->m_NextFrameTime = ms_per_frame;
enemy->m_Explode->Run();
large_explosion_snd->Play();
}
else {
enemy->ElasticCollision( asteroid );
small_explosion_snd->Play();
}
}
}
现在有两个电话打给ElasticCollision
。其中一个召唤发生在玩家飞船与小行星相撞,玩家飞船的护盾升起的时候。另一种情况发生在敌舰与小行星相撞时,敌舰将护盾竖起。
我们必须对collisions()
函数做的最后一个改变是增加一个新的双asteroid
环,它将环绕我们的所有小行星,寻找它们之间的碰撞。这样就产生了一种有趣的效果,小行星会像台球一样相互弹开。如果探测到两个小行星之间发生碰撞,我们称之为ElasticCollision
:
Asteroid* asteroid_1;
Asteroid* asteroid_2;
std::vector<Asteroid*>::iterator ita_1;
std::vector<Asteroid*>::iterator ita_2;
for( ita_1 = asteroid_list.begin(); ita_1 != asteroid_list.end(); ita_1++ ) {
asteroid_1 = *ita_1;
if( !asteroid_1->m_Active ) {
continue;
}
for( ita_2 = ita_1+1; ita_2 != asteroid_list.end(); ita_2++ ) {
asteroid_2 = *ita_2;
if( !asteroid_2->m_Active ) {
continue;
}
if( asteroid_1->HitTest( asteroid_2 ) ) {
asteroid_1->ElasticCollision( asteroid_2 );
}
}
}
我们必须对asteroid.cpp
和projectile.cpp
做一个小的补充。我们在Collider
类中增加了一个名为m_Mass
的新属性,所以所有从Collider
派生的类都继承了这个属性。我们的ElasticCollision
功能使用m_Mass
属性来确定这些对象在弹性碰撞后将如何移动。宇宙飞船的质量和射弹的质量之比将被用来计算宇宙飞船发射射弹时产生的后坐力。首先修改的是Projectile
类的构造函数。下面是该构造函数的新版本:
Projectile::Projectile(): Collider(4.0) {
m_Active = false;
SDL_Surface *temp_surface = IMG_Load( c_SpriteFile );
if( !temp_surface ) {
printf("failed to load image: %s\n", IMG_GetError() );
return;
}
m_SpriteTexture = SDL_CreateTextureFromSurface( renderer, temp_surface
);
if( !m_SpriteTexture ) {
printf("failed to create texture: %s\n", IMG_GetError() );
return;
}
SDL_FreeSurface( temp_surface );
m_Mass = 1.0;
}
唯一的修改是最后一行,我们将m_Mass
设置为1.0
:
m_Mass = 1.0;
下一个需要修改的构造函数在asteroid.cpp
文件中。我们需要修改Asteroid
类的构造函数。以下是新版本的Asteroid
建造师:
Asteroid::Asteroid( float x, float y, float velocity, float rotation ): Collider(8.0) {
SDL_Surface *temp_surface = IMG_Load( ADSTEROID_SPRITE_FILE );
if( !temp_surface ) {
printf("failed to load image: %s\n", IMG_GetError() );
return;
}
else { printf("success creating asteroid surface\n"); }
m_SpriteTexture = SDL_CreateTextureFromSurface( renderer, temp_surface
);
if( !m_SpriteTexture ) {
printf("failed to create texture: %s\n", IMG_GetError() );
return;
}
else { printf("success creating asteroid texture\n"); }
SDL_FreeSurface( temp_surface );
m_Explode = new Emitter((char*)"/sprites/Explode.png", 100, 0, 360,
1000, 0.3, false, 20.0, 40.0, 10, 0, 0, 5, 1.0, 2.0, 1.0, 2.0,
0xffffff, 0xffffff, 0.01, 10, false, false, 800, 8 );
m_Explode->m_parent_rotation_ptr = &m_Rotation;
m_Explode->m_parent_x_ptr = &(m_Position.x);
m_Explode->m_parent_y_ptr = &(m_Position.y);
m_Explode->m_Active = false;
m_Chunks = new Emitter((char*)"/sprites/small-asteroid.png",40,0,360,
1000, 0.05, false, 80.0, 150.0, 5,0,0,10,2.0,2.0,0.25, 0.5, 0xffffff,
0xffffff, 0.1, 10, false, true, 1000, 8 );
m_Chunks->m_parent_rotation_ptr = &m_Rotation;
m_Chunks->m_parent_x_ptr = &m_Position.x;
m_Chunks->m_parent_y_ptr = &m_Position.y;
m_Chunks->m_Active = false;
m_Position.x = x;
m_Position.y = y;
Vector2D direction;
direction.x = 1;
direction.Rotate( rotation );
m_Direction = direction;
m_Velocity = m_Direction * velocity;
m_dest.h = m_src.h = m_dest.w = m_src.w = 16;
m_Rotation = rotation;
m_Active = true;
m_CurrentFrame = 0;
m_NextFrameTime = ms_per_frame;
m_Mass = 100.0;
}
同样,我们将添加的唯一一行是我们将m_Mass
设置为100.0
的最后一行:
m_Mass = 100.0;
对ship.cpp
文件的第一个更改将是对Ship
构造函数的更改。这是我们需要对构造函数进行的一个简单的更改,在这里我们将把船的质量设置为50.0
。以下是新版本的Ship
类构造函数:
Ship::Ship() : Collider(8.0) {
m_Rotation = PI;
m_LastLaunchTime = current_time;
m_Accelerating = false;
m_Exhaust = new Emitter((char*)"/sprites/ProjectileExpOrange.png", 200,
-10, 10,
400, 1.0, true,
0.1, 0.1,
30, 0, 12, 0.5,
0.5, 1.0,
0.5, 1.0,
0xffffff, 0xffffff,
0.7, 10,
true, true,
1000, 6 );
m_Exhaust->m_parent_rotation_ptr = &m_Rotation;
m_Exhaust->m_parent_x_ptr = &(m_Position.x);
m_Exhaust->m_parent_y_ptr = &(m_Position.y);
m_Exhaust->m_x_adjustment = 10;
m_Exhaust->m_y_adjustment = 10;
m_Exhaust->m_Active = false;
m_Explode = new Emitter((char*)"/sprites/Explode.png", 100,
0, 360,
1000, 0.3, false,
20.0, 40.0,
10, 0, 0, 5,
1.0, 2.0,
1.0, 2.0,
0xffffff, 0xffffff,
0.0, 10,
false, false,
800, 8 );
m_Explode->m_parent_rotation_ptr = &m_Rotation;
m_Explode->m_parent_x_ptr = &(m_Position.x);
m_Explode->m_parent_y_ptr = &(m_Position.y);
m_Explode->m_Active = false;
m_Direction.y = 1.0;
m_Active = true;
m_Mass = 50.0;
}
唯一被更改的一行是最后一行:
m_Mass = 50.0;
我们还需要改变Shoot
功能来增加后坐力。将添加几条线,通过添加一个与船面对的方向相反的矢量来修改船的速度,该矢量的大小基于发射的射弹的速度和相对质量。以下是新的Shoot
功能:
void Ship::Shoot() {
Projectile* projectile;
if( current_time - m_LastLaunchTime >= c_MinLaunchTime ) {
m_LastLaunchTime = current_time;
projectile = projectile_pool->GetFreeProjectile();
if( projectile != NULL ) {
projectile->Launch( m_Position, m_Direction );
player_laser_snd->Play();
m_Velocity -= m_Direction * (projectile->c_Velocity * projectile->m_Mass /
m_Mass);
CapVelocity();
}
}
}
这是我们添加到函数中的两行:
m_Velocity -= m_Direction * (projectile->c_Velocity * projectile->m_Mass / m_Mass);
CapVelocity();
现在我们已经添加了物理,是时候编译我们的代码了。我们可以使用以下em++
命令构建physics.html
文件:
em++ asteroid.cpp audio.cpp camera.cpp collider.cpp emitter.cpp enemy_ship.cpp finite_state_machine.cpp locator.cpp main.cpp particle.cpp player_ship.cpp projectile_pool.cpp projectile.cpp range.cpp render_manager.cpp shield.cpp ship.cpp star.cpp vector.cpp -o physics.html --preload-file audio --preload-file sprites -std=c++ 17 -s USE_WEBGL2=1 -s USE_SDL=2 -s USE_SDL_IMAGE=2 -s SDL2_IMAGE_FORMATS=["png"] -s USE_SDL_IMAGE=2 -s SDL2_IMAGE_FORMATS=["png"]
下面的截图看起来可能与早期版本相似,但是当你发射炮弹时,飞船会向后加速。如果你在护盾开启时与小行星相撞,你会像台球一样被它们弹开。离太阳太近,重力会开始吸引你的船:
Figure 13.1: physics.html screenshot
在这一章中,我们讨论了计算机游戏中的物理学史,以及这段历史是如何追溯到第一个计算机游戏 SpaceWar 的!。我们讨论了我们游戏中已经有的物理,包括动量守恒。我们简要讨论了牛顿第三定律及其如何应用于游戏,然后我们通过使用第三定律为我们的游戏添加了更多的牛顿物理学。我们给我们的恒星增加了一个引力场,让它在我们的游戏中用一个随着两个物体之间距离的平方而减小的力来吸引宇宙飞船。最后,我们增加了宇宙飞船、射弹和小行星之间的弹性碰撞。
在下一章中,我们将为我们的游戏添加一个用户界面 ( UI )。我们还将把游戏分成多个屏幕,并增加一个鼠标界面。