相机设计是游戏新手设计师经常忘记的事情之一。到目前为止,我们已经有了所谓的固定位置摄像机。只有一个屏幕,视角没有变化。20 世纪 70 年代,几乎所有早期的街机游戏都是这样设计的。我发现的用任何相机拍摄的最古老的游戏是雅达利的月球着陆器,它于 1979 年 8 月发布。月球着陆器是一个早期的基于矢量的游戏,当着陆器接近月球表面时,它会放大相机,然后当你的着陆器接近表面时,它会平移相机来跟随。
20 世纪 80 年代初,更多的游戏开始尝试一个比单个游戏屏幕更大的游戏世界。拉力赛 X 是南科在 1980 年发布的一款 Pac-Man- 之类的迷宫游戏,迷宫比单个显示器还要大。拉力赛 X 使用了一个位置抓拍摄像头(有时被称为锁定摄像头),无论发生什么情况,该摄像头始终将玩家的车保持在游戏屏幕的中央。这是你可以实现的最直接的 2D 滚动相机形式,许多游戏新手设计师会创建一个 2D 位置抓拍相机然后收工,但是你可能希望在你的游戏中实现一个更复杂的相机是有原因的。
中途岛在 1981 年发布了游戏防御者。这是一个侧滚射击游戏,玩家可以向任何方向移动他们的飞船。意识到玩家需要在飞船面对的方向上看到更多的水平,防御者使用了第一个双前焦摄像头。这个摄像头会移动观看区域,让三分之二的屏幕在玩家飞船面对的方向前面,三分之一的屏幕在后面。这就把更多的焦点放在了玩家面前。相机不只是在两个位置之间来回切换。那会很不和谐。相反,当玩家切换方向时,相机位置会平稳地转换到新位置(对 1981 年来说相当酷)。
20 世纪 80 年代,许多新的相机设计开始使用。Konami 开始在他们的许多射击游戏中使用自动滚动相机,包括 Scramble 、 Gradius 和 1942 。1985 年,雅达利发布了战书,这是一款早期的多人游戏,允许四名玩家同时参与游戏。排管中的摄像头定位在玩家所有位置的平均值。平台游戏,如超级马里奥兄弟,将允许用户向前推动相机的位置。
You will need to include several images in your build to make this project work. Make sure you include the /Chapter11/sprites/
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.
如果你花时间去看看,2D 相机有很多很好的例子。我们将集中(无意双关)一些对我们的游戏有帮助的 2D 相机功能。
我们将在几个不同的阶段制造我们的相机。我们将从裸机锁定摄像头实现开始。这将为我们添加新的相机功能提供一个良好的起点。稍后,我们将把这款相机修改为投影对焦相机。投射式对焦相机会观察玩家飞船的速度,并调整相机,以便在玩家面前显示更多的游戏区域。这种技术的工作原理是基于这样一种假设,即在这个游戏中,玩家通常更专注于玩家飞船移动方向上的游戏性。对于我们相机的最终版本,我们将在投射物中添加相机 吸引子。这种修改背后的想法是,当游戏中有射击时,相机应该将注意力吸引到游戏的那个区域。
我们相机的第一个实现将是一个锁定的相机,它将锁定我们的玩家,并跟随他们在关卡中的区域移动。现在,我们的关卡和那个关卡的固定摄像头一样大。我们不仅需要使我们的水平更大,而且我们还需要修改我们的对象包装,以便它与我们的相机一起工作。要实现我们的锁定相机,我们需要做的第一件事就是修改我们的game.hpp
文件。我们将创建一个Camera
类和一个RenderManager
类,在那里我们将移动所有渲染特定的代码。我们还需要添加一些#define
宏来定义我们级别的高度和宽度,因为这将不同于我们已经定义的画布高度和宽度。我们还将在我们的Vector2D
类中添加一些额外的重载操作符。
锁定摄像头并不是一件可怕的事情,但是更好的摄像头可以显示玩家需要看到的更多内容。在我们的游戏中,玩家更有可能对他们前进的方向感兴趣。在运动方向上向前看的照相机有时被称为投射聚焦照相机。我们可以查看我们的船当前移动的速度,并相应地偏移我们的相机。
我们将采用的另一种摄像技术叫做摄像吸引器。有时在游戏中,有一些感兴趣的对象可以用来拉/吸引相机的焦点。这些会产生一种吸引力,将我们的相机拉向那个方向。我们相机的一个吸引力是敌舰。另一个吸引力是射弹。敌人的船代表潜在的行动,投射物代表对我们玩家的潜在威胁。在本节中,我们将结合投影焦点和相机吸引器来改善我们的相机定位。
最后我想补充的是一个箭头,它指向敌人的宇宙飞船。因为现在的游戏区域比画布还大,我们需要一个提示来帮助我们找到敌人。没有这一点,我们可能会发现自己漫无目的地闲逛,这不是很有趣。另一种方法是用迷你地图,但是,因为只有一个敌人,我觉得箭更容易实现。让我们浏览一下我们需要添加的代码,以改进我们的相机,并添加我们的定位箭头。
我们需要为这一章添加几个新的类。显然,如果我们在游戏中想要一个摄像头,我们将需要添加一个Camera
类。在代码的早期版本中,渲染是通过直接调用 SDL 来完成的。因为 SDL 没有相机作为 API 的一部分,所以我们需要添加一个RenderManager
类,作为渲染过程中的中间步骤。这个类将使用摄像机的位置来决定我们将在画布上的什么地方渲染我们的游戏对象。我们将增加我们的游戏区域到四个屏幕宽和四个屏幕高。这就产生了一个游戏性的问题,因为现在,我们在玩的时候需要能够找到敌人的飞船。为了解决这个问题,我们需要创建一个定位器用户界面 ( 用户界面)元素,该元素将箭头指向敌人飞船的方向。
让我们浏览一下我们将对game.hpp
文件进行的更改。我们从添加几个#define
宏开始:
#define LEVEL_WIDTH CANVAS_WIDTH*4
#define LEVEL_HEIGHT CANVAS_HEIGHT*4
这将定义我们关卡的宽度和高度是画布宽度和高度的四倍。在我们的类列表的末尾,我们应该添加一个Camera
类、Locator
类和RenderManager
类,如下所示:
class Ship;
class Particle;
class Emitter;
class Collider;
class Asteroid;
class Star;
class PlayerShip;
class EnemyShip;
class Projectile;
class ProjectilePool;
class FiniteStateMachine;
class Camera;
class RenderManager;
class Locator;
您会注意到最后三行声明一个名为Camera
的类、一个名为Locator
的类和一个名为RenderManager
的类将在代码的后面定义。
我们将扩展我们的Vector2D
类定义,为我们的Vector2D
类中的+
和-
操作符添加一个operator+
和operator-
重载。
If you are not familiar with operator overloading, these are a convenient way to allow classes to use C++ operators instead of functions. There is a good tutorial that can help if you are looking for more information that is available at https://www.tutorialspoint.com/cplusplus/cpp_overloading.htm.
以下是Vector2D
类的新定义:
class Vector2D {
public:
float x;
float y;
Vector2D();
Vector2D( float X, float Y );
void Rotate( float radians );
void Normalize();
float MagSQ();
float Magnitude();
Vector2D Project( Vector2D &onto );
float Dot(Vector2D &vec);
float FindAngle();
Vector2D operator=(const Vector2D &vec);
Vector2D operator*(const float &scalar);
void operator+=(const Vector2D &vec);
void operator-=(const Vector2D &vec);
void operator*=(const float &scalar);
void operator/=(const float &scalar);
Vector2D operator-(const Vector2D &vec);
Vector2D operator+(const Vector2D &vec);
};
您会注意到定义的最后两行是新的:
Vector2D operator-(const Vector2D &vec);
Vector2D operator+(const Vector2D &vec);
Locator
类是一个 UI 元素的新类,它将是一个箭头,将我们的玩家指向敌人飞船的方向。我们需要一个 UI 元素来帮助玩家在敌人飞船没有出现在画布上的时候找到它。下面是类定义的样子:
class Locator {
public:
bool m_Active = false;
bool m_LastActive = false;
SDL_Texture *m_SpriteTexture;
SDL_Rect m_dest = {.x = 0, .y = 0, .w = 32, .h = 32 };
Vector2D m_Position;
int m_ColorFlux;
float m_Rotation;
Locator();
void SetActive();
void Move();
void Render();
};
前两个属性是布尔标志,与定位器的活动状态有关。m_Active
属性告诉我们定位器当前是否活动,是否应该渲染。m_LastActive
属性是一个布尔标志,它告诉我们上次渲染帧时定位器是否处于活动状态。接下来的两行是 sprite 纹理和目标矩形,渲染管理器将使用它们来渲染这个游戏对象:
SDL_Texture *m_SpriteTexture;
SDL_Rect m_dest = {.x = 0, .y = 0, .w = 32, .h = 32 };
之后,我们在m_Position
属性中有一个x
和y
位置值,m_ColorFlux
中有一个代表 RGB 颜色值的整数,m_Rotation
属性中有一个子画面的旋转值。我们将使用m_ColorFlux
属性使箭头的颜色在敌人靠近时更红,在敌人更远时更白。
这个类定义的最后四行是类函数。有一个构造函数,一个将定位器状态设置为激活的函数,Move
和Render
函数:
Locator();
void SetActive();
void Move();
void Render();
我们现在需要添加新的Camera
类定义。这个类将用于定义我们的viewport
和我们的摄像机的位置。每一帧都会调用Move
功能。最初,Move
会锁定我们玩家的位置,在关卡周围跟随。稍后,我们将更改此功能以创建更动态的相机。这就是Camera
班的样子:
class Camera {
public:
Vector2D m_Position;
float m_HalfWidth;
float m_HalfHeight;
Camera( float width, float height );
void Move();
};
一直以来,我们都是在没有背景的情况下在自己的水平上移动。这在前面几章中很好,我们的关卡正好适合画布元素。然而,现在我们正在用相机滚动我们的水平。如果背景中没有任何东西在移动,很难判断你的飞船是否在移动。为了在我们的游戏中创建运动的错觉,我们需要添加一个背景渲染器。除此之外,我们希望游戏中的所有渲染都使用我们刚刚创建的相机作为偏移来完成。正因为如此,我们不再希望我们的游戏对象直接调用SDL_RenderCopy
或者SDL_RenderCopyEx
。相反,我们创建了一个RenderManager
类,负责在我们的游戏中执行渲染。我们有一个RenderBackground
功能,将渲染一个星空作为背景,我们创建了一个Render
功能,将渲染我们的雪碧纹理使用相机作为偏移。这就是RenderManager
类定义的样子:
class RenderManager {
public:
const int c_BackgroundWidth = 800;
const int c_BackgroundHeight = 600;
SDL_Texture *m_BackgroundTexture;
SDL_Rect m_BackgroundDest = {.x = 0, .y = 0, .w =
c_BackgroundWidth, .h = c_BackgroundHeight };
RenderManager();
void RenderBackground();
void Render( SDL_Texture *tex, SDL_Rect *src, SDL_Rect *dest, float
rad_rotation = 0.0, int alpha = 255, int red = 255, int green =
255, int blue = 255 );
};
我们在game.hpp
文件中需要做的最后一件事是创建一个到两个新的Camera
和RenderManager
类型的对象指针的外部链接。这些是我们将在这个版本的游戏引擎中使用的相机和渲染管理器对象,并且是我们将在main.cpp
文件中定义的变量的外部引用:
extern Camera* camera;
extern RenderManager* render_manager;
extern Locator* locator;
我们在Camera
类中定义了两个函数;我们的camera
对象和Move
函数的构造函数,我们将使用它来跟踪我们的player
对象。以下是我们在camera.cpp
文件中的内容:
#include "game.hpp"
Camera::Camera( float width, float height ) {
m_HalfWidth = width / 2;
m_HalfHeight = height / 2;
}
void Camera::Move() {
m_Position = player->m_Position;
m_Position.x -= CANVAS_WIDTH / 2;
m_Position.y -= CANVAS_HEIGHT / 2;
}
在这个实现中,Camera
构造函数和Move
函数是非常简单的。构造函数根据传入的宽度和高度设置摄像机的半宽半高。Move
功能将摄像机的位置设置为玩家的位置,然后将摄像机的位置移动画布宽度和画布高度的一半,使玩家居中。我们刚刚构建了一个入门相机,并将在本章后面的内容中添加更多功能。
我们将把所有我们用来渲染对象内部精灵的调用转移到RenderManager
类。我们需要这样做,因为我们将使用我们的相机的位置来决定我们将在画布上的哪里渲染精灵。我们还需要一个功能来渲染我们的背景星域。我们的render_manager.cpp
文件的前几行将包括game.hpp
文件,并定义我们的背景图像的虚拟文件系统位置:
#include "game.hpp"
#define BACKGROUND_SPRITE_FILE (char*)"/sprites/starfield.png"
之后,我们将定义我们的构造函数。构造函数将用于加载我们的starfield.png
文件作为SDL_Surface
对象,然后将使用该表面创建一个SDL_Texture
对象,我们将使用它来渲染我们的背景:
RenderManager::RenderManager() {
SDL_Surface *temp_surface = IMG_Load( BACKGROUND_SPRITE_FILE );
if( !temp_surface ) {
printf("failed to load image: %s\n", IMG_GetError() );
return;
}
m_BackgroundTexture = SDL_CreateTextureFromSurface( renderer,
temp_surface );
if( !m_BackgroundTexture ) {
printf("failed to create texture: %s\n", IMG_GetError() );
return;
}
SDL_FreeSurface( temp_surface );
}
RenderBackground
函数需要在我们在main
循环中定义的render()
函数的开头调用。因此,RenderBackground
的前两行将有两个函数,我们将使用这两个函数将先前从main.cpp
中的render()
函数调用的渲染器清除为黑色:
SDL_SetRenderDrawColor( renderer, 0, 0, 0, 255 );
SDL_RenderClear( renderer );
之后,我们将设置一个背景矩形作为渲染目的地。starfield.png
的大小与我们的画布大小(800 x 600)匹配,所以我们需要根据相机的位置渲染四次。因为这是一个重复的纹理,所以我们可以在相机的位置上使用模运算符(%
)来计算我们想要如何偏移 starfield。举个例子,如果我们把相机放在*x* = 100
、*y* = 200
上,我们会想要在-100
、-200
上渲染我们的星际背景的第一个副本。如果我们停在那里,右边会有 100 像素的黑色空间,画布底部会有 200 像素的黑色空间。因为我们想在这些领域的背景,我们将需要三个额外的渲染我们的背景。如果我们在700
、-200
第二次渲染我们的背景(将画布宽度添加到先前渲染的 x 值),我们现在将在画布底部有一个 200 像素的黑色条带。然后,我们可以在-100
、400
处渲染我们的星域(将画布高度添加到原始渲染的 y 值中)。这将使我们在底部角落有一个 100 x 200 像素的黑色。第四个渲染需要将画布宽度和画布高度添加到原始渲染的 x 和 y 值中,以填充该角落。这就是在RenderBackground
功能中正在发生的事情,我们使用该功能根据摄像机的位置将重复的背景渲染到画布上:
void RenderManager::RenderBackground() {
SDL_SetRenderDrawColor( renderer, 0, 0, 0, 255 );
SDL_RenderClear( renderer );
SDL_Rect background_rect = {.x = 0, .y=0, .w=CANVAS_WIDTH,
.h=CANVAS_HEIGHT};
int start_x = (int)(camera->m_Position.x) % CANVAS_WIDTH;
int start_y = (int)(camera->m_Position.y) % CANVAS_HEIGHT;
background_rect.x -= start_x;
background_rect.y -= start_y;
SDL_RenderCopy( renderer, m_BackgroundTexture, NULL,
&background_rect );
background_rect.x += CANVAS_WIDTH;
SDL_RenderCopy( renderer, m_BackgroundTexture, NULL,
&background_rect );
background_rect.x -= CANVAS_WIDTH;
background_rect.y += CANVAS_HEIGHT;
SDL_RenderCopy( renderer, m_BackgroundTexture, NULL,
&background_rect );
background_rect.x += CANVAS_WIDTH;
SDL_RenderCopy( renderer, m_BackgroundTexture, NULL,
&background_rect );
}
我们在render_manager.cpp
中定义的最后一个函数是我们的Render
函数。在定义了这个函数之后,我们需要找到我们之前在代码中调用过SDL_RenderCopy
和SDL_RenderCopyEx
的每个地方,并用对渲染管理器的Render
函数的调用来替换这些调用。这个功能不仅会根据我们相机的位置渲染我们的精灵,还会用来设置颜色和 alpha 通道的修改。以下是Render
功能的全部代码:
void RenderManager::Render( SDL_Texture *tex, SDL_Rect *src, SDL_Rect *dest, float rad_rotation,int alpha, int red, int green, int blue ) {
SDL_Rect camera_dest = *dest;
if( camera_dest.x <= CANVAS_WIDTH &&
camera->m_Position.x >= LEVEL_WIDTH - CANVAS_WIDTH ) {
camera_dest.x += (float)LEVEL_WIDTH;
}
else if( camera_dest.x >= LEVEL_WIDTH - CANVAS_WIDTH &&
camera->m_Position.x <= CANVAS_WIDTH ) {
camera_dest.x -= (float)LEVEL_WIDTH;
}
if( camera_dest.y <= CANVAS_HEIGHT &&
camera->m_Position.y >= LEVEL_HEIGHT - CANVAS_HEIGHT ) {
camera_dest.y += (float)LEVEL_HEIGHT;
}
else if( camera_dest.y >= LEVEL_HEIGHT - CANVAS_HEIGHT &&
camera->m_Position.y <= CANVAS_HEIGHT ) {
camera_dest.y -= (float)LEVEL_HEIGHT;
}
camera_dest.x -= (int)camera->m_Position.x;
camera_dest.y -= (int)camera->m_Position.y;
SDL_SetTextureAlphaMod(tex,
(Uint8)alpha );
SDL_SetTextureColorMod(tex,
(Uint8)red,
(Uint8)green,
(Uint8)blue );
if( rad_rotation != 0.0 ) {
float degree_rotation = RAD_TO_DEG(rad_rotation);
SDL_RenderCopyEx( renderer, tex, src, &camera_dest,
degree_rotation, NULL, SDL_FLIP_NONE );
}
else {
SDL_RenderCopy( renderer, tex, src, &camera_dest );
}
}
这个函数做的第一件事是创建一个新的SDL_Rect
对象,我们将使用它来修改传递给Render
函数的dest
变量中的值。因为我们有一个包裹 x 和 y 坐标的关卡,如果我们在关卡的右边,我们会想要将关卡最左边的物体渲染到右边。同样,如果我们在我们级别的最左侧,我们将希望将位于我们级别最右侧的对象渲染到我们的右侧。这使得我们的宇宙飞船可以从我们水平的左侧循环回到我们水平的右侧,反之亦然。以下是调整相机位置以将对象环绕到关卡左侧和右侧的代码:
if( camera_dest.x <= CANVAS_WIDTH &&
camera->m_Position.x >= LEVEL_WIDTH - CANVAS_WIDTH ) {
camera_dest.x += (float)LEVEL_WIDTH;
}
else if( camera_dest.x >= LEVEL_WIDTH - CANVAS_WIDTH &&
camera->m_Position.x <= CANVAS_WIDTH ) {
camera_dest.x -= (float)LEVEL_WIDTH;
}
完成此操作后,我们将做一些类似的事情,以允许在我们级别的顶部和底部包装对象的位置:
if( camera_dest.y <= CANVAS_HEIGHT &&
camera->m_Position.y >= LEVEL_HEIGHT - CANVAS_HEIGHT ) {
camera_dest.y += (float)LEVEL_HEIGHT;
}
else if( camera_dest.y >= LEVEL_HEIGHT - CANVAS_HEIGHT &&
camera->m_Position.y <= CANVAS_HEIGHT ) {
camera_dest.y -= (float)LEVEL_HEIGHT;
}
接下来,我们需要从camera_dest
x 和 y 坐标中减去摄像机的位置,并设置我们的alpha
和color
mod 的值:
camera_dest.x -= (int)camera->m_Position.x;
camera_dest.y -= (int)camera->m_Position.y;
SDL_SetTextureAlphaMod(tex,
(Uint8)alpha );
SDL_SetTextureColorMod(tex,
(Uint8)red,
(Uint8)green,
(Uint8)blue );
在函数的末尾,如果我们的精灵旋转了,我们将调用SDL_RenderCopyEx
,如果没有旋转,我们将调用SDL_RenderCopy
:
if( rad_rotation != 0.0 ) {
float degree_rotation = RAD_TO_DEG(rad_rotation);
SDL_RenderCopyEx( renderer, tex, src, &camera_dest,
degree_rotation, NULL, SDL_FLIP_NONE );
}
else {
SDL_RenderCopy( renderer, tex, src, &camera_dest );
}
为了实现我们的相机,我们需要对我们的main.cpp
文件进行几次修改。我们需要为我们的相机、渲染管理器和定位器添加一些新的全局变量。我们将需要修改我们的move
功能,以包括移动我们的相机和定位器的调用。我们将修改我们的render
功能来渲染我们的背景和定位器。最后,我们需要给我们的main
函数添加更多的初始化代码。
我们需要在我们的main.cpp
文件的开头附近创建三个新的全局变量。我们需要指向RenderManager
、Camera
和Locator
的对象指针。这些声明是这样的:
Camera* camera;
RenderManager* render_manager;
Locator* locator;
我们需要修改我们的move
功能来移动我们的相机和定位器对象。我们需要在move
函数的末尾添加以下两行:
camera->Move();
locator->Move();
以下是整个move
功能:
void move() {
player->Move();
enemy->Move();
projectile_pool->MoveProjectiles();
Asteroid* asteroid;
std::vector<Asteroid*>::iterator it;
int i = 0;
for( it = asteroid_list.begin(); it != asteroid_list.end(); it++ ) {
asteroid = *it;
if( asteroid->m_Active ) {
asteroid->Move();
}
}
star->Move();
camera->Move();
locator->Move();
}
我们将在render
函数的最开始添加一行。这条线将渲染背景星空并根据摄像机位置移动它:
render_manager->RenderBackground();
之后,我们需要在render
函数的末尾添加一行。该行需要在SDL_RenderPresent
调用之前立即出现,仍然需要是该功能中的最后一行:
locator->Render();
这就是render()
函数的整体外观:
void render() {
render_manager->RenderBackground();
player->Render();
enemy->Render();
projectile_pool->RenderProjectiles();
Asteroid* asteroid;
std::vector<Asteroid*>::iterator it;
for( it = asteroid_list.begin(); it != asteroid_list.end(); it++ ) {
asteroid = *it;
asteroid->Render();
}
star->Render();
locator->Render();
SDL_RenderPresent( renderer );
}
最后的修改将是在main
功能中发生的初始化。我们需要为前面定义的camera
、render_manager
和locator
指针创建新对象:
camera = new Camera(CANVAS_WIDTH, CANVAS_HEIGHT);
render_manager = new RenderManager();
locator = new Locator();
在之前的代码版本中,我们有 7 次调用new Asteroid
并使用asteroid_list.push_back
将这 7 个新的小行星推入我们的小行星列表。我们现在需要创建比七个小行星多得多的小行星,因此,我们将使用双for
循环来创建小行星并将其分散到整个游戏区域,而不是单独调用它们。要做到这一点,我们首先需要删除所有早期创建和推送小行星的调用:
asteroid_list.push_back( new Asteroid(
200, 50, 0.05,
DEG_TO_RAD(10) ) );
asteroid_list.push_back( new Asteroid(
600, 150, 0.03,
DEG_TO_RAD(350) ) );
asteroid_list.push_back( new Asteroid(
150, 500, 0.05,
DEG_TO_RAD(260) ) );
asteroid_list.push_back( new Asteroid(
450, 350, 0.01,
DEG_TO_RAD(295) ) );
asteroid_list.push_back( new Asteroid(
350, 300, 0.08,
DEG_TO_RAD(245) ) );
asteroid_list.push_back( new Asteroid(
700, 300, 0.09,
DEG_TO_RAD(280) ) );
asteroid_list.push_back( new Asteroid(
200, 450, 0.03,
DEG_TO_RAD(40) ) );
一旦您删除了前面的所有代码,我们将添加以下代码来创建我们的新小行星,并在整个游戏区域中半随机地分隔它们:
int asteroid_x = 0;
int asteroid_y = 0;
int angle = 0;
// SCREEN 1
for( int i_y = 0; i_y < 8; i_y++ ) {
asteroid_y += 100;
asteroid_y += rand() % 400;
asteroid_x = 0;
for( int i_x = 0; i_x < 12; i_x++ ) {
asteroid_x += 66;
asteroid_x += rand() % 400;
int y_save = asteroid_y;
asteroid_y += rand() % 400 - 200;
angle = rand() % 359;
asteroid_list.push_back( new Asteroid(
asteroid_x, asteroid_y,
get_random_float(0.5, 1.0),
DEG_TO_RAD(angle) ) );
asteroid_y = y_save;
}
}
现在我们正在使用渲染管理器来渲染我们所有的游戏对象,我们将需要遍历我们的各种游戏对象,并修改它们以通过渲染管理器而不是直接渲染。我们要修改的第一个文件是asteroid.cpp
。在asteroid.cpp
里面,我们有Asteroid::Render()
功能。在前几章中,这个函数将通过调用SDL_RenderCopyEx
直接在 SDL 渲染小行星精灵。现在我们有了我们在main.cpp
文件中定义的render_manager
对象,我们将使用该渲染管理器来间接渲染我们的精灵。RenderManager::Render
功能将使用相机调整画布上渲染精灵的位置。我们需要对Asteroid::Render()
功能进行的第一个修改是删除以下几行:
SDL_RenderCopyEx( renderer, m_SpriteTexture,
&m_src, &m_dest,
RAD_TO_DEG(m_Rotation), NULL, SDL_FLIP_NONE );
移除对SDL_RenderCopyEX
的调用后,我们需要在render_manager
对象中添加对Render
函数的以下调用:
render_manager->Render( m_SpriteTexture, &m_src, &m_dest, m_Rotation );
新版本的Asteroid::Render
功能现在将如下所示:
void Asteroid::Render() {
m_Explode->Move();
m_Chunks->Move();
if( m_Active == false ) {
return;
}
m_src.x = m_dest.w * m_CurrentFrame;
m_dest.x = m_Position.x + m_Radius / 2;
m_dest.y = m_Position.y + m_Radius / 2;
render_manager->Render( m_SpriteTexture, &m_src, &m_dest, m_Rotation );
}
我们需要修改collider.cpp
文件中的一个功能。先前版本的WrapPosition
功能检查一个Collider
物体是否从画布上移到一边或者另一边,如果是这样,该功能将把碰撞器移到另一边。这模仿了经典的雅达利街机游戏小行星的行为。在雅达利小行星中,如果一颗小行星或玩家的飞船在一侧移出屏幕,那颗小行星(或飞船)就会出现在游戏屏幕的另一侧。以下是我们的wrap
代码的前一个版本:
void Collider::WrapPosition() {
if( m_Position.x > CANVAS_WIDTH + m_Radius ) {
m_Position.x = -m_Radius;
}
else if( m_Position.x < -m_Radius ) {
m_Position.x = CANVAS_WIDTH;
}
if( m_Position.y > CANVAS_HEIGHT + m_Radius ) {
m_Position.y = -m_Radius;
}
else if( m_Position.y < -m_Radius ) {
m_Position.y = CANVAS_HEIGHT;
}
}
因为我们的游戏现在扩展到了单个画布之外,所以如果一个对象离开了画布,我们就不再想要包装了。相反,如果对象超出了级别界限,我们希望将其环绕。以下是新版本的WrapPosition
功能:
void Collider::WrapPosition() {
if( m_Position.x > LEVEL_WIDTH ) {
m_Position.x -= LEVEL_WIDTH;
}
else if( m_Position.x < 0 ) {
m_Position.x += LEVEL_WIDTH;
}
if( m_Position.y > LEVEL_HEIGHT ) {
m_Position.y -= LEVEL_HEIGHT;
}
else if( m_Position.y < 0 ) {
m_Position.y += LEVEL_HEIGHT;
}
}
有必要对enemy_ship.cpp
文件进行一个小的修改。EnemyShip
构造函数现在将在m_Position
属性上设置x
和y
值。我们需要将位置设置为810
和800
,因为现在级别比画布大小大很多。我们将在EnemyShip
构造函数的最顶端设置m_Position
属性。这是更改后构造函数的开头:
EnemyShip::EnemyShip() {
m_Position.x = 810.0;
m_Position.y = 800.0;
我们需要对finite_state_machine.cpp
文件进行一个小的修改。在FiniteStateMachine::AvoidForce()
功能中,有几个对画布尺寸的引用,现在我们的级别大小和画布大小不同,必须更改这些引用才能引用级别尺寸。之前,我们已经将star_avoid
变量的x
和y
属性设置为以下基于画布的值:
star_avoid.x = CANVAS_WIDTH / 2;
star_avoid.y = CANVAS_HEIGHT / 2;
这些线必须改为参考LEVEL_WIDTH
和LEVEL_HEIGHT
:
star_avoid.x = LEVEL_WIDTH / 2;
star_avoid.y = LEVEL_HEIGHT / 2;
我们必须对avoid_vec
变量做同样的事情。以下是我们之前的内容:
avoid_vec.x = CANVAS_WIDTH / 2;
avoid_vec.y = CANVAS_HEIGHT / 2;
也必须改为参考LEVEL_WIDTH
和LEVEL_HEIGHT
:
avoid_vec.x = LEVEL_WIDTH / 2;
avoid_vec.y = LEVEL_HEIGHT / 2;
FiniteState::AvoidForce
功能的新版本整体如下:
void FiniteStateMachine::AvoidForce() {
Vector2D start_corner;
Vector2D end_corner;
Vector2D avoid_vec;
Vector2D dist;
float closest_square = 999999999999.0;
float msq;
Vector2D star_avoid;
star_avoid.x = LEVEL_WIDTH / 2;
star_avoid.y = LEVEL_HEIGHT / 2;
star_avoid -= m_Ship->m_Position;
msq = star_avoid.MagSQ();
if( msq >= c_StarAvoidDistSQ ) {
start_corner = m_Ship->m_Position;
start_corner.x -= c_AvoidDist;
start_corner.y -= c_AvoidDist;
end_corner = m_Ship->m_Position;
end_corner.x += c_AvoidDist;
end_corner.y += c_AvoidDist;
Asteroid* asteroid;
std::vector<Asteroid*>::iterator it;
int i = 0;
for( it = asteroid_list.begin(); it != asteroid_list.end(); it++ ) {
asteroid = *it;
if( asteroid->m_Active == true &&
asteroid->SteeringRectTest( start_corner, end_corner ) ) {
dist = asteroid->m_Position;
dist -= m_Ship->m_Position;
msq = dist.MagSQ();
if( msq <= closest_square ) {
closest_square = msq;
avoid_vec = asteroid->m_Position;
}
}
}
// LOOP OVER PROJECTILES
Projectile* projectile;
std::vector<Projectile*>::iterator proj_it;
for( proj_it = projectile_pool->m_ProjectileList.begin();
proj_it != projectile_pool->m_ProjectileList.end(); proj_it++ ) {
projectile = *proj_it;
if( projectile->m_Active == true &&
projectile->SteeringRectTest( start_corner, end_corner ) ) {
dist = projectile->m_Position;
dist -= m_Ship->m_Position;
msq = dist.MagSQ();
if( msq <= closest_square ) {
closest_square = msq;
avoid_vec = projectile->m_Position;
}
}
}
if( closest_square != 999999999999.0 ) {
avoid_vec -= m_Ship->m_Position;
avoid_vec.Normalize();
float rot_to_obj = avoid_vec.FindAngle();
if( std::abs( rot_to_obj - m_Ship->m_Rotation ) < 0.75 ) {
if( rot_to_obj >= m_Ship->m_Rotation ) {
m_Ship->RotateLeft();
}
else {
m_Ship->RotateRight();
}
}
m_Ship->m_Velocity -= avoid_vec * delta_time *
c_ObstacleAvoidForce;
}
}
else {
avoid_vec.x = LEVEL_WIDTH / 2;
avoid_vec.y = LEVEL_HEIGHT / 2;
avoid_vec -= m_Ship->m_Position;
avoid_vec.Normalize();
float rot_to_obj = avoid_vec.FindAngle();
if( std::abs( rot_to_obj - m_Ship->m_Rotation ) < 0.75 ) {
if( rot_to_obj >= m_Ship->m_Rotation ) {
m_Ship->RotateLeft();
}
else {
m_Ship->RotateRight();
}
}
m_Ship->m_Velocity -= avoid_vec * delta_time * c_StarAvoidForce;
}
}
我们需要修改particle.cpp
文件中的Render
函数,通过render_manager
渲染粒子,而不是直接通过调用 SDL。Particle::Render
功能的旧版本如下:
void Particle::Render() {
SDL_SetTextureAlphaMod(m_sprite_texture,
(Uint8)m_alpha );
if( m_color_mod == true ) {
SDL_SetTextureColorMod(m_sprite_texture,
m_current_red,
m_current_green,
m_current_blue );
}
if( m_align_rotation == true ) {
SDL_RenderCopyEx( renderer, m_sprite_texture, &m_src, &m_dest,
m_rotation, NULL, SDL_FLIP_NONE );
}
else {
SDL_RenderCopy( renderer, m_sprite_texture, &m_src, &m_dest );
}
}
新的Particle::Render
函数将通过render_manager
对象对Render
函数进行一次调用:
void Particle::Render() {
render_manager->Render( m_sprite_texture, &m_src, &m_dest, m_rotation,
m_alpha, m_current_red, m_current_green, m_current_blue );
}
我们需要对player_ship.cpp
文件进行一个小的修改。就像我们对enemy_ship.cpp
文件所做的更改一样,我们需要添加两行来设置m_Position
属性中的x
和y
值。
我们需要删除PlayerShip::PlayerShip()
构造函数的前两行:
m_Position.x = CANVAS_WIDTH - 210.0;
m_Position.y = CANVAS_HEIGHT - 200.0;
这些是我们需要对PlayerShip::PlayerShip()
构造函数进行的更改:
PlayerShip::PlayerShip() {
m_Position.x = LEVEL_WIDTH - 810.0;
m_Position.y = LEVEL_HEIGHT - 800.0;
我们需要对projectile.cpp
文件进行一个小的修改。与其他游戏对象一样,Render
函数先前直接调用 SDL 函数来渲染游戏对象。我们需要通过render_manager
对象打电话,而不是打给 SDL。我们需要从Projectile::Render()
功能中删除以下行:
int return_val = SDL_RenderCopy( renderer, m_SpriteTexture,
&src, &dest );
if( return_val != 0 ) {
printf("SDL_Init failed: %s\n", SDL_GetError());
}
代替这些行,我们需要添加对render_manager
对象上的Render
函数的调用:
render_manager->Render( m_SpriteTexture, &src, &dest );
这就是新版本的Projectile::Render()
功能的样子:
void Projectile::Render() {
dest.x = m_Position.x + 8;
dest.y = m_Position.y + 8;
dest.w = c_Width;
dest.h = c_Height;
src.x = 16 * m_CurrentFrame;
render_manager->Render( m_SpriteTexture, &src, &dest );
}
与许多其他游戏对象一样,Shield::Render()
函数将需要修改,以便它不再直接调用 SDL,而是从render_manager
对象调用Render
函数。在Shield::Render()
功能中,我们需要删除对 SDL 的以下呼叫:
SDL_SetTextureColorMod(m_SpriteTexture,
color_red,
color_green,
0 );
SDL_RenderCopyEx( renderer, m_SpriteTexture,
&m_src, &m_dest,
RAD_TO_DEG(m_Ship->m_Rotation),
NULL, SDL_FLIP_NONE );
我们将用对Render
的一次呼叫来替换这些线路:
render_manager->Render( m_SpriteTexture, &m_src, &m_dest, m_Ship->m_Rotation,
255, color_red, color_green, 0 );
这就是新版本的Shield::Render
功能的整体外观:
void Shield::Render() {
if( m_Active ) {
int color_green = m_ttl / 100 + 1;
int color_red = 255 - color_green;
m_src.x = m_CurrentFrame * m_dest.w;
m_dest.x = m_Ship->m_Position.x;
m_dest.y = m_Ship->m_Position.y;
render_manager->Render( m_SpriteTexture, &m_src, &m_dest, m_Ship->m_Rotation,
255, color_red, color_green, 0 );
}
}
在我们的游戏对象中修改Render
功能变得非常常规。与我们修改了Render
功能的其他对象一样,我们需要删除所有到 SDL 的直接呼叫。以下是我们需要从Render
功能中删除的代码:
float degrees = (m_Rotation / PI) * 180.0;
int return_code = SDL_RenderCopyEx( renderer, m_SpriteTexture,
&src, &dest,
degrees, NULL, SDL_FLIP_NONE );
if( return_code != 0 ) {
printf("failed to render image: %s\n", IMG_GetError() );
}
删除这些行后,我们需要添加一行来调用render_manager->Render
函数:
render_manager->Render( m_SpriteTexture, &src, &dest, m_Rotation );
我们需要修改star.cpp
文件中的两个函数。首先,我们需要在Star::Star()
构造函数中修改星的位置。在上一章的Star
构造函数版本中,我们将星星的位置设置在画布的中间。现在,它必须被设置到级别的中间。以下是构造函数原始版本中的行:
m_Position.x = CANVAS_WIDTH / 2;
m_Position.y = CANVAS_HEIGHT / 2;
我们现在将这些更改为相对于LEVEL_WIDTH
和LEVEL_HEIGHT
的位置,而不是相对于CANVAS_WIDTH
和CANVAS_HEIGHT
的位置:
m_Position.x = LEVEL_WIDTH / 2;
m_Position.y = LEVEL_HEIGHT / 2;
在对Star::Star
构造函数进行上述更改后,我们需要对Star::Render
函数进行更改。我们需要删除对SDL_RenderCopy
的调用,并替换为对render_manager
对象上的Render
函数的调用。这就是之前版本的Render
功能的样子:
void Star::Render() {
Emitter* flare;
std::vector<Emitter*>::iterator it;
for( it = m_FlareList.begin(); it != m_FlareList.end(); it++ ) {
flare = *it;
flare->Move();
}
m_src.x = m_dest.w * m_CurrentFrame;
SDL_RenderCopy( renderer, m_SpriteTexture,
&m_src, &m_dest );
}
我们将修改如下:
void Star::Render() {
Emitter* flare;
std::vector<Emitter*>::iterator it;
for( it = m_FlareList.begin(); it != m_FlareList.end(); it++ ) {
flare = *it;
flare->Move();
}
m_src.x = m_dest.w * m_CurrentFrame;
render_manager->Render( m_SpriteTexture, &m_src, &m_dest );
}
我们需要在Vector2D
类中添加两个新的重载操作符。我们需要超越operator-
和operator+
。这段代码非常简单。它将使用已经超载的operator-=
和operator+=
来允许我们互相加减向量。下面是这些重载操作符的新代码:
Vector2D Vector2D::operator-(const Vector2D &vec) {
Vector2D return_vec = *this;
return_vec -= vec;
return return_vec;
}
Vector2D Vector2D::operator+(const Vector2D &vec) {
Vector2D return_vec = *this;
return_vec += vec;
return return_vec;
}
如果我们编译并测试我们现在拥有的东西,我们应该能够在我们的水平周围移动,并看到一个直接跟踪玩家位置的摄像机。我们应该有一个定位箭头来帮助我们找到敌人的飞船。下面是对 Emscripten 的命令行调用,我们可以用它来构建我们的项目:
em++ asteroid.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 index.html --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"]
在 Windows 或 Linux 命令提示符下运行前一行。运行此程序后,从网络服务器提供index.html
文件,并在浏览器(如 Chrome 或 Firefox)中打开它。
我们现在的相机是功能性的,但是有点无聊。它只关注玩家,这没什么问题,但可以显著改进。首先,正如 Defender 的设计者所意识到的,更重要的是将摄像头的焦点放在玩家移动的方向,而不是直接对准玩家。为了实现这一点,我们将添加投影焦点到我们的相机。这将会看到玩家飞船的当前速度,并且会在这个速度的方向上向前移动相机。然而,有时你可能仍然希望你的相机的焦点在播放器后面。为了对此有所帮助,我们将添加一些相机吸引器。照相机吸引器是将照相机的注意力吸引到它们身上的物体。如果敌人出现在玩家身后,可能更重要的是稍微向后移动相机,以帮助将敌人保持在屏幕上。如果敌人正在向你射击,将摄像机对准向你飞来的射弹可能更重要。
我们需要做的第一个改变是我们的games.hpp
文件。让摄像机跟着我们的玩家很容易。相机没有任何啪嗒声或震动,因为玩家的船不会那样移动。如果我们打算使用更高级的功能,如吸引子和前焦点,我们将需要计算相机的期望位置,然后平滑地过渡到该位置。为了支持这一点,我们需要给我们的Camera
类添加一个m_DesiredPosition
属性。以下是我们必须添加的新行:
Vector2D m_DesiredPosition;
这就是我们的games.hpp
文件中的Camera
类在添加之后的样子:
class Camera {
public:
Vector2D m_Position;
Vector2D m_DesiredPosition;
float m_HalfWidth;
float m_HalfHeight;
Camera( float width, float height );
void Move();
};
现在我们已经在类定义中添加了一个期望的位置属性,我们需要更改我们的camera.cpp
文件。我们需要修改构造函数,将摄像机的位置设置为玩家飞船的位置。以下是我们需要添加到构造函数中的行:
m_Position = player->m_Position;
m_Position.x -= CANVAS_WIDTH / 2;
m_Position.y -= CANVAS_HEIGHT / 2;
下面是我们添加这些行后的构造函数:
Camera::Camera( float width, float height ) {
m_HalfWidth = width / 2;
m_HalfHeight = height / 2;
m_Position = player->m_Position;
m_Position.x -= CANVAS_WIDTH / 2;
m_Position.y -= CANVAS_HEIGHT / 2;
}
我们的Camera::Move
功能将完全不同。你不妨删除当前版本Camera::Move
中的所有代码行,因为它们都不再有用了。我们新的期望位置属性将在Move
功能开始时设置,就像之前设置位置一样。为此,在您通过删除该函数中的所有内容而创建的空版本Camera::Move
中添加以下行:
m_DesiredPosition = player->m_Position;
m_DesiredPosition.x -= CANVAS_WIDTH / 2;
m_DesiredPosition.y -= CANVAS_HEIGHT / 2;
如果玩家不在了,我们会希望我们的相机稳定在这个位置。玩家死了之后,我们就不希望任何吸引物影响相机的位置了。玩家死亡后过多移动玩家摄像头看起来有些奇怪,所以添加以下几行代码,检查玩家的飞船是否激活,如果没有,则将摄像头的位置移向想要的位置,然后从Move
功能返回:
if( player->m_Active == false ) {
m_Position.x = m_Position.x + (m_DesiredPosition.x - m_Position.x)
* delta_time;
m_Position.y = m_Position.y + (m_DesiredPosition.y - m_Position.y)
* delta_time;
return;
}
我们将在我们的游戏吸引器中制造所有的主动射弹。如果有敌人向我们射击,这是对我们船只的威胁,因此应该引起摄像机的注意。如果我们发射射弹,这也表明了我们聚焦的方向。我们将使用一个for
循环来循环我们游戏中的所有投射物,如果那个投射物是活动的,我们将使用它的位置来移动我们相机的期望位置。下面是代码:
Projectile* projectile;
std::vector<Projectile*>::iterator it;
Vector2D attractor;
for( it = projectile_pool->m_ProjectileList.begin(); it != projectile_pool->m_ProjectileList.end(); it++ ) {
projectile = *it;
if( projectile->m_Active ) {
attractor = projectile->m_Position;
attractor -= player->m_Position;
attractor.Normalize();
attractor *= 5;
m_DesiredPosition += attractor;
}
}
在使用我们的吸引器移动相机的期望位置后,我们将根据玩家船只的速度修改m_DesiredPosition
变量,代码如下:
m_DesiredPosition += player->m_Velocity * 2;
因为我们的关卡是环绕的,如果你从关卡的一边退出,你会出现在另一边,我们需要调整相机的位置来解决这个问题。如果没有以下几行代码,当玩家在一侧移出关卡边界并在另一侧重新出现时,摄像机会突然发出刺耳的声音:
if( abs(m_DesiredPosition.x - m_Position.x) > CANVAS_WIDTH ) {
if( m_DesiredPosition.x > m_Position.x ) {
m_Position.x += LEVEL_WIDTH;
}
else {
m_Position.x -= LEVEL_WIDTH;
}
}
if( abs(m_DesiredPosition.y - m_Position.y) > CANVAS_HEIGHT ) {
if( m_DesiredPosition.y > m_Position.y ) {
m_Position.y += LEVEL_HEIGHT;
}
else {
m_Position.y -= LEVEL_HEIGHT;
}
}
最后,我们将添加几行代码来平滑地将摄像机的当前位置转换到所需位置。我们使用delta_time
来使这个转换花费大约一秒钟。直接设置我们的相机位置,而不是使用所需的位置和过渡,会导致新的吸引人进入游戏时动作不平稳。下面是过渡代码:
m_Position.x = m_Position.x + (m_DesiredPosition.x - m_Position.x) *
delta_time;
m_Position.y = m_Position.y + (m_DesiredPosition.y - m_Position.y) *
delta_time;
现在我们已经分别看到了我们的Move
函数的所有行,让我们来看看这个函数的完整新版本:
void Camera::Move() {
m_DesiredPosition = player->m_Position;
m_DesiredPosition.x -= CANVAS_WIDTH / 2;
m_DesiredPosition.y -= CANVAS_HEIGHT / 2;
if( player->m_Active == false ) {
m_Position.x = m_Position.x + (m_DesiredPosition.x - m_Position.x)
* delta_time;
m_Position.y = m_Position.y + (m_DesiredPosition.y - m_Position.y)
* delta_time;
return;
}
Projectile* projectile;
std::vector<Projectile*>::iterator it;
Vector2D attractor;
for( it = projectile_pool->m_ProjectileList.begin();
it != projectile_pool->m_ProjectileList.end(); it++ ) {
projectile = *it;
if( projectile->m_Active ) {
attractor = projectile->m_Position;
attractor -= player->m_Position;
attractor.Normalize();
attractor *= 5;
m_DesiredPosition += attractor;
}
}
m_DesiredPosition += player->m_Velocity * 2;
if( abs(m_DesiredPosition.x - m_Position.x) > CANVAS_WIDTH ) {
if( m_DesiredPosition.x > m_Position.x ) {
m_Position.x += LEVEL_WIDTH;
}
else {
m_Position.x -= LEVEL_WIDTH;
}
}
if( abs(m_DesiredPosition.y - m_Position.y) > CANVAS_HEIGHT ) {
if( m_DesiredPosition.y > m_Position.y ) {
m_Position.y += LEVEL_HEIGHT;
}
else {
m_Position.y -= LEVEL_HEIGHT;
}
}
m_Position.x = m_Position.x + (m_DesiredPosition.x - m_Position.x) *
delta_time;
m_Position.y = m_Position.y + (m_DesiredPosition.y - m_Position.y) *
delta_time;
}
当你建造了这个版本,你会注意到相机在你的船移动的方向前进。如果你开始拍摄,它会走得更远。当敌人的宇宙飞船靠近,并向你射击时,相机也应该向那些射弹的方向漂移。和以前一样,您可以通过在 Windows 或 Linux 命令提示符下输入以下代码来编译和测试代码:
em++ asteroid.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 camera.html --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 11.1: New camera version with lines added to divide the screen
如你所见,相机没有对准玩家飞船的中心。相机的焦点主要投射在玩家船的速度方向,由于敌舰和抛射体的原因,会稍微向右上方拖动。
Do not forget that you must run WebAssembly apps using a web server, or with emrun
. If you would like to run your WebAssembly app using emrun
, you must compile it with the --emrun
flag. The web browser requires a web server to stream the WebAssembly module. If you attempt to open an HTML page that uses WebAssembly in a browser directly from your hard drive, that WebAssembly module will not load.
我们从学习视频游戏中摄像头的历史开始这一章。我们讨论的第一台相机是最简单的相机,有时被称为锁定相机。那是一种能精确跟踪玩家位置的摄像机。之后,我们了解了 2D 太空中锁定摄像头的替代方案,包括引导玩家的摄像头。我们讨论了投影对焦相机,以及它们如何预测玩家的移动,并根据玩家移动的方向向前投影相机的位置。然后,我们讨论了相机吸引子,以及它们如何将相机的焦点吸引到感兴趣的对象上。在讨论了相机的类型后,我们创建了一个相机对象,并将其设计为实现投影焦点和相机吸引器。我们实现了一个渲染管理器,并修改了我们所有的游戏对象来通过RenderManager
类进行渲染。然后,我们创建了一个locator
对象,以帮助我们在敌人的宇宙飞船不再出现在画布上时找到它。
在下一章中,我们将学习如何为我们的游戏添加音效。