有了碰撞检测后,我们就可以完成一些更复杂的游戏了,我们使用VS2019创建新项目“D3D_07_Mario”,这个案例主要演示2D超级马里奥游戏加入碰撞检测的代码。本案例将会重新构建所有代码和素材,和之前的项目“D3D_06_Mario”有很大的差别。我们即将创建的新项目“D3D_07_Mario”除了完成之前的功能之外,新增加了静态物体和怪物两种游戏对象,以及使用碰撞盒进行碰撞检测。本案例中,我们将依次封装精灵类(四边形纹理),地图背景类,静态物体类,怪物类,玩家类和正交摄像机类。首先,我们介绍精灵(四边形纹理)类,这个是最基本的,我们新建“SpriteClass.h”和“SpriteClass.cpp”两个文件,其中“SpriteClass.h”文件内容如下:
#pragma once
#include "main.h"
// 定义FVF灵活顶点格式
struct D3D_SPRITE_DATA_VERTEX { FLOAT x, y, z, u, v; };
// 定义包含纹理的顶点类型
#define D3D_SPRITE_FVF_VERTEX (D3DFVF_XYZ | D3DFVF_TEX1)
// 精灵类,就是一个四边形纹理贴图
class SpriteClass {
public:
float x, y; // 位置
float width, height; // 尺寸
bool isShow; // 是否绘制
// 顶点缓冲区对象
LPDIRECT3DVERTEXBUFFER9 buffer;
// 顶点数组
D3D_SPRITE_DATA_VERTEX vertexes[6];
// 纹理数组长度和当前待绘制的纹理索引
int len, cur;
// 纹理贴图对象数组
LPDIRECT3DTEXTURE9* texture;
// Direct3D设备指针对象
LPDIRECT3DDEVICE9 D3DDevice = NULL;
public:
// 构造函数
SpriteClass() {};
SpriteClass(LPDIRECT3DDEVICE9 _device, float _x, float _y, float _w, float _h);
// 生成四边形
void quad();
// 载入贴图
void load(std::wstring files[], int size);
// 渲染图形
void render();
// 子类继承实现函数
virtual void update() {};
// 析构函数
~SpriteClass();
};
在精灵这个基础类中,我们定义了位置和尺寸基本属性,还定义了一个是否绘制的变量。同时,我们声明了一个纹理数组,用于加载多种图片的情况。这里重点要说的是update函数,这是一个虚函数,也就是说它必须由子类来完成。该方法的添加,主要目的是为了让精灵子类本身能够处理用户交互逻辑。我们之前讲过,游戏的主体就是一个无限循环,在这个无限循环中,不停的处理用户交互,然后渲染游戏画面,而这两个行为动作又是通过游戏数据来连接在一起的。也就是说,用户交互后修改游戏数据,然后根据游戏数据来渲染游戏画面。而这两个行为动作对应的就是update函数和render函数。因此,所有的游戏对象,都将会使用这两个函数来完成自己的游戏逻辑流程,或者与其他游戏对象的交互游戏逻辑流程。而在我们的main.cpp代码中,只需要管理所有的游戏对象,并在main.cpp主体的update和render函数中分别调用每个游戏对象的update函数和render函数即可。这里,需要注意的一个问题,就是update函数和render函数是一起调用执行的,只不过分工不同而已。在我们之前的所有案例中,我们都是接收到操作系统的事件消息后,才调用update函数,然后紧接着调用render函数。这个逻辑是不对的,正确的应该是,即使没有事件消息,也要调用update函数。这样做的目的是为了解决游戏数据的修改,不一定是用户触发的事件。例如,怪物的自动行走,我们就需要在update方法中完成。这个变动其实很简单,就是在我们的wWinMain入口函数的第五步消息循环过程位置,在循环体的else中添加一句“update(0, NULL);”即可。这就是我们对游戏代码框架的一种设计。明白了大致思路之后,我们就来开始完成“SpriteClass.cpp”的代码,如下:
#include "SpriteClass.h"
// 精灵类:构造函数
SpriteClass::SpriteClass(LPDIRECT3DDEVICE9 _device, float _x, float _y, float _w, float _h) {
x = _x;
y = _y;
width = _w;
height = _h;
isShow = false;
D3DDevice = _device;
quad();//生成四边形
};
// 精灵类:生成四边形
void SpriteClass::quad() {
// 定义四边形顶点数组
vertexes[0] = { 0.0f, 0.0f, 0.0f, 0.0f, 1.0f };
vertexes[1] = { 0.0f, height,0.0f, 0.0f, 0.0f };
vertexes[2] = { width, height,0.0f, 1.0f, 0.0f };
vertexes[3] = { 0.0f, 0.0f, 0.0f, 0.0f, 1.0f };
vertexes[4] = { width, height,0.0f, 1.0f, 0.0f };
vertexes[5] = { width, 0.0f, 0.0f, 1.0f, 1.0f };
// 创建顶点缓存
D3DDevice->CreateVertexBuffer(sizeof(vertexes), 0, D3D_SPRITE_FVF_VERTEX, D3DPOOL_DEFAULT, &buffer, NULL);
// 访问顶点缓存
void* ptr;
buffer->Lock(0, sizeof(vertexes), (void**)&ptr, 0);
memcpy(ptr, vertexes, sizeof(vertexes));
buffer->Unlock();
};
// 精灵类:载入贴图
void SpriteClass::load(std::wstring files[], int size) {
cur = 0;
len = size;
texture = new LPDIRECT3DTEXTURE9[len];
for (int i = 0; i < len; i++) {
LPCWSTR temp = (LPCWSTR)files[i].c_str();
D3DXCreateTextureFromFile(D3DDevice, temp, &(texture[i]));
}
};
// 精灵类:渲染图形
void SpriteClass::render() {
// 不绘制图形
if (isShow == false) return;
// 世界矩阵
D3DXMATRIX matrix;
D3DXMatrixTranslation(&matrix, x, y, 0);
D3DDevice->SetTransform(D3DTS_WORLD, &matrix);
// 开启Alpha融合
D3DDevice->SetRenderState(D3DRS_ALPHABLENDENABLE, true);
// 设置混合因子
D3DDevice->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);
D3DDevice->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);
// 设置融合运算方式(相加)
D3DDevice->SetRenderState(D3DRS_BLENDOP, D3DBLENDOP_ADD);
// 设置纹理
if (NULL != texture[cur]) D3DDevice->SetTexture(0, texture[cur]);
// 绘制四边形
D3DDevice->SetStreamSource(0, buffer, 0, sizeof(D3D_SPRITE_DATA_VERTEX));
D3DDevice->SetFVF(D3D_SPRITE_FVF_VERTEX);
D3DDevice->DrawPrimitive(D3DPT_TRIANGLELIST, 0, 2);
// 关闭Alpha混合
D3DDevice->SetRenderState(D3DRS_ALPHABLENDENABLE, false);
};
// 精灵类:析构函数
SpriteClass::~SpriteClass() {
for (int i = 0; i < len; i++) {
if (NULL == texture[i]) continue;
texture[i]->Release();
texture[i] = NULL;
}
delete[] texture;
buffer->Release();
buffer = NULL;
};
以上代码都是之前的知识。接下来,我们继续抽象地图类,它继承上面的精灵类,创建“MapClass.h”和“MapClass.cpp”两个文件,其中“MapClass.h”代码如下:
#pragma once
#include "SpriteClass.h"
// 地图类,继承精灵类
class MapClass : public SpriteClass {
public:
// 构造方法
MapClass(LPDIRECT3DDEVICE9 device, std::wstring file);
// 重写父类update函数
void update();
};
有了精灵类的继承,地图类就非常的简单了。下面是“MapClass.cpp”代码:
#include "MapClass.h"
// 地图类: 构造方法
MapClass::MapClass(LPDIRECT3DDEVICE9 device, std::wstring file) {
// Direct3D设备指针对象
D3DDevice = device;
// 定义背景纹理位置
x = 0, y = -30;
// 定义背景纹理尺寸
width = 740, height = 240;
// 载入纹理
len = 1, cur = 0;
LPCWSTR temp = (LPCWSTR)file.c_str();
texture = new LPDIRECT3DTEXTURE9[len];
D3DXCreateTextureFromFile(D3DDevice, temp, &(texture[cur]));
// 显示纹理
isShow = true;
// 生成四边形
quad();
};
// 地图类: 重写父类update函数
void MapClass::update() {};
在这里,我们并没有调用精灵类的构造方法,而是自己重新构造数据,这样做就是方便一些。接下来,我们将创建“CollideBox.h”和“CollideBox.cpp”两个文件,他们主要用于2D碰撞盒检测。首先是“CollideBox.h”,代码如下:
#pragma once
#include "main.h"
#include "SpriteClass.h"
// 碰撞位置检测
enum CollidePositioin { NO, LEFT, RIGHT, UP, DOWN };
// 2D碰撞盒
class CollideBox {
private:
float xMin;
float xMax;
float yMin;
float yMax;
public:
// 构造函数,参数是精灵的所有顶点坐标数据列表
CollideBox(D3D_SPRITE_DATA_VERTEX* list, int len);
// 更新碰撞盒位置
void updateCollide(float x, float y);
// 单一方向更新碰撞盒
void updateCollide(bool isX, float x);
// 检查两个碰撞盒是否碰撞
CollidePositioin isCollideBox(CollideBox* box);
};
这里,我们基本延续之前的封装思路。使用顶点数组来声明一个构造函数,也就是四边形四个顶点数据。然后是碰撞盒位置更新,包括两个轴向的更新和一个轴向的更新。最后就是两个碰撞盒的检测函数。“CollideBox.cpp”代码如下:
#include "CollideBox.h"
// 2D碰撞盒:构造函数,参数是精灵的所有顶点坐标数据列表
CollideBox::CollideBox(D3D_SPRITE_DATA_VERTEX* list, int len) {
xMin = 0;
xMax = 0;
yMin = 0;
yMax = 0;
for (int i = 0; i < len; i++) {
D3D_SPRITE_DATA_VERTEX temp = list[i];
if (temp.x < xMin) xMin = temp.x;
if (temp.y < yMin) yMin = temp.y;
if (temp.x > xMax) xMax = temp.x;
if (temp.y > yMax) yMax = temp.y;
}
};
// 2D碰撞盒:更新碰撞盒位置
void CollideBox::updateCollide(float x, float y) {
xMin += x;
xMax += x;
yMin += y;
yMax += y;
};
// 2D碰撞盒:单一方向更新碰撞盒
void CollideBox::updateCollide(bool isX, float x) {
if (isX) {
xMin += x;
xMax += x;
}
else {
yMin += x;
yMax += x;
}
};
// 2D碰撞盒:检查两个碰撞盒是否碰撞
CollidePositioin CollideBox::isCollideBox(CollideBox* box) {
// 是否碰撞检测
CollidePositioin pos = NO;
if (xMax < (*box).xMin || xMin >(*box).xMax) return pos;
if (yMax < (*box).yMin || yMin >(*box).yMax) return pos;
// 碰撞位置检测,这里还是有问题
if ((xMax > (*box).xMin) && yMin < 20) pos = LEFT;
if ((xMin < (*box).xMax) && yMin < 20) pos = RIGHT;
if ((yMin < (*box).yMax) && yMin > 20) pos = UP;
if ((yMax > (*box).yMin) && yMin > 20) pos = DOWN;
return pos;
};
这里需要注意的一点是,我们增加了碰撞位置的检测。这个在碰撞检测中非常关键。有了碰撞盒之后,我们就可以创建静态物体类了,新建“StaticObjectClass.h”和“StaticObjectClass.cpp”两个文件,其中“StaticObjectClass.h”文件如下:
#pragma once
#include "CollideBox.h"
#include "SpriteClass.h"
// 静态物体类,继承精灵类,拥有碰撞盒
class StaticObjectClass : public SpriteClass {
public:
// 碰撞盒
CollideBox* box;
public:
// 构造方法
StaticObjectClass(LPDIRECT3DDEVICE9 device, std::wstring file, float _x, float _y, float _w, float _h);
// 重写父类update函数,在渲染 render 函数中开始位置执行
void update();
};
静态物体和地图类的区别在于,它拥有碰撞盒。那么“StaticObjectClass.cpp”代码如下:
#include "StaticObjectClass.h"
// 静态物体类: 构造方法
StaticObjectClass::StaticObjectClass(LPDIRECT3DDEVICE9 device, std::wstring file, float _x, float _y, float _w, float _h) {
// Direct3D设备指针对象
D3DDevice = device;
// 定义背景纹理位置
x = _x, y = _y;
// 定义背景纹理尺寸
width = _w, height = _h;
// 载入纹理
len = 1, cur = 0;
LPCWSTR temp = (LPCWSTR)file.c_str();
texture = new LPDIRECT3DTEXTURE9[len];
D3DXCreateTextureFromFile(D3DDevice, temp, &(texture[cur]));
// 显示纹理
isShow = true;
// 生成四边形
quad();
// 计算碰撞盒
box = new CollideBox(vertexes, 6);
box->updateCollide(_x, _y);
};
// 静态物体类: 重写父类update函数
void StaticObjectClass::update() {};
我们同样也是使用自己的构造方法来对数据进行初始化。静态物体创建完毕后,我们将创建“MonsterClass.h”和“MonsterClass.cpp”两个文件,用于怪物类的实现。首先是“MonsterClass.h”文件内容:
#pragma once
#include "CollideBox.h"
#include "SpriteClass.h"
// 怪物类,继承精灵类,拥有碰撞盒
class MonsterClass : public SpriteClass {
public:
int direction; // 移动方向(0不移动,1向右,-1向左)
float step; // 移动步长
CollideBox* box; // 碰撞盒
bool isCollideStopLeftMove; // 碰撞停止向左移动
bool isCollideStopRightMove;// 碰撞停止向右移动
public:
// 构造函数,调用父类构造函数
MonsterClass(LPDIRECT3DDEVICE9 _device, float _x, float _y, float _w, float _h);
// 重写父类update函数
void update();
// 解析动画纹理贴图索引cur
int getCur();
// 移动
void moveX();
// 碰撞检测回调
void collideCallback(std::wstring tag, CollidePositioin pos);
};
这个怪物类的设计有一点复杂,它需要支持动画和碰撞检测。首先动画其实就是控制纹理数组的下标变量cur。碰撞检测代码的设计思路是这样,首先在main.cpp中的renderScene函数中完成碰撞检测,然后将检测的结果回调给碰撞检测的两个游戏对象身上。这样,碰撞的实际逻辑处理仍然放到了游戏对象类中完成。这样做的目的还是能够让游戏对象类来完成所有的游戏逻辑处理。getCur函数就是用来调整纹理数组下标变量cur的,而moveX函数就是用来控制怪物位置移动和行走动画的。很显然,getCur函数和moveX函数需要在update函数中完成。接下来我们仔细看一看“MonsterClass.cpp”文件代码:
#include "MonsterClass.h"
// 怪物类: 构造函数,调用父类构造函数
MonsterClass::MonsterClass(LPDIRECT3DDEVICE9 _device, float _x, float _y, float _w, float _h) : SpriteClass(_device, _x, _y, _w, _h) {
// 计算碰撞盒
box = new CollideBox(vertexes, 6);
box->updateCollide(_x, _y);
// 初始化纹理贴图
std::wstring files[] = {
L"asset/mushroom1.bmp",
L"asset/mushroom2.bmp",
L"asset/mushroom0.bmp" };
int size = sizeof(files) / sizeof(files[0]);
load(files, size);
// 默认纹理索引
cur = 0;
// 默认绘制
isShow = true;
// 移动步长
direction = 1;
step = 1.0f;
// 碰撞停止移动
isCollideStopLeftMove = false;
isCollideStopRightMove = false;
};
// 怪物类: 重写父类update函数
void MonsterClass::update() {
// 修改纹理贴图索引值(形成动画)
cur = getCur();
// 移动
moveX();
};
// 怪物类: 解析动画纹理贴图索引cur
int MonsterClass::getCur() {
if (cur == 0) return 1;
else return 0;
};
// 怪物类: 移动
void MonsterClass::moveX() {
// 不移动
if (direction == 0) return;
// 发生碰撞相反移动
if (isCollideStopLeftMove && direction == -1) direction = 1;
if (isCollideStopRightMove && direction == 1) direction = -1;
// 修改怪物X轴坐标位置
x += (step * direction);
// 怪物超出屏幕后消失
if (x < 0 || x > 710) { isShow = false; return; }
// 更新碰撞盒(X轴)
box->updateCollide(true, step * direction);
};
// 怪物类: 碰撞检测回调
void MonsterClass::collideCallback(std::wstring tag, CollidePositioin pos) {
// 如果遇到静态物体
if (tag == L"pipe") {
// 停止移动
if (pos == LEFT) isCollideStopLeftMove = true;
if (pos == RIGHT) isCollideStopRightMove = true;
// 恢复移动
if (pos == NO) {
if (isCollideStopLeftMove) isCollideStopLeftMove = false;
if (isCollideStopRightMove) isCollideStopRightMove = false;
}
}
// 如果遇到玩家,并且碰撞点在上边或者下边的话,怪物死亡
if (tag == L"player" && (pos == UP || pos == DOWN)) {
isShow = false;
}
};
首先,在构造函数中,我们调用了父类的构造函数用于初始化四边形,然后我们就使用四边形来计算碰撞盒,紧接着加载纹理,其中第一个用于死亡,第二个和第三个纹理用于行走。
在update方法中,我们就使用了getCur和moveX函数,让怪物能够自行移动。getCur函数非常简单,因为我们的行走贴图就两个,所以只需要频繁变更这两个下标即可。然后就是我们的移动moveX函数,里面的主要逻辑就是更新X坐标值,以及发生碰撞后,移动方向取反,最后就是碰撞盒的更新。在碰撞检测中,我们传递了两个参数,一个是碰撞对象,一个是碰撞位置。碰撞的逻辑代码根据碰撞对象的不同而不同。如果是静态物体的话,则停止移动且改变方向。我们知道移动的代码是在moveX函数中实现的,但是碰撞要对移动产生影响,因此两者之间需要数据变量进行传递控制。这就是isCollideStopLeftMove和isCollideStopRightMove两个数据变量存在的意义。如果碰撞的是玩家的话,且碰撞位置是UP或Down的话,则怪物死亡。怪物死亡的处理,只是不渲染,这样做的目的是重复利用该怪物。怪物类介绍完毕后,就是我们的马里奥玩家类了。但是,在此之前,我们还需要创建一个正交摄像机类。于是我们先创建“CameraClass.h”和“CameraClass.cpp”两个文件,其中“CameraClass.h”文件内容如下:
#pragma once
#include "main.h"
// 摄像机类
class CameraClass {
public:
// 摄像机位置
float eyeX = 0.0f, eyeY = 0.0f, eyeZ = 0.0f;
// 摄像机观察点
float lookAtX = 0.0f, lookAtY = 0.0f, lookAtZ = 0.0f;
// 正交投影区域大小
float viewWidth = 0.0f, viewHeight = 0.0f;
// Direct3D设备指针对象
LPDIRECT3DDEVICE9 D3DDevice = NULL;
public:
// 构造方法
CameraClass(LPDIRECT3DDEVICE9 device, float ex, float ey, float ez, float lx, float ly, float lz, float w, float h);
// 初始化变换矩阵
void init();
// 更新变换矩阵,只考虑X轴的变换
void update(float direction, float step);
};
这个正交摄像机非常的简单,它只考虑在X轴方向上跟随玩家移动。下面是“CameraClass.cpp”文件的实现:
#include "CameraClass.h"
// 摄像机类: 构造方法
CameraClass::CameraClass(LPDIRECT3DDEVICE9 device, float ex, float ey, float ez, float lx, float ly, float lz, float w, float h) {
// 摄像机位置
eyeX = ex;
eyeY = ey;
eyeZ = ez;
// 摄像机观察点
lookAtX = lx;
lookAtY = ly;
lookAtZ = lz;
// 正交投影区域大小
viewWidth = w;
viewHeight = h;
// Direct3D设备指针对象
D3DDevice = device;
};
// 摄像机类: 初始化变换矩阵
void CameraClass::init() {
// 设置取景变换矩阵
D3DXMATRIX viewMatrix;
D3DXVECTOR3 viewEye(eyeX, eyeY, eyeZ);
D3DXVECTOR3 viewLookAt(lookAtX, lookAtY, lookAtZ);
D3DXVECTOR3 viewUp(0.0f, 1.0f, 0.0f);
D3DXMatrixLookAtLH(&viewMatrix, &viewEye, &viewLookAt, &viewUp);
D3DDevice->SetTransform(D3DTS_VIEW, &viewMatrix);
// 正交投影变换
D3DXMATRIX matProject;
D3DXMatrixOrthoLH(&matProject, viewWidth, viewHeight, 0, 100);
D3DDevice->SetTransform(D3DTS_PROJECTION, &matProject);
// 视口变换
D3DVIEWPORT9 viewport = { 0, 0, WINDOW_WIDTH, WINDOW_HEIGHT, 0, 1 };
D3DDevice->SetViewport(&viewport);
};
// 摄像机类: 更新变换矩阵,只考虑X轴的变换
void CameraClass::update(float direction, float step) {
eyeX += (direction * step);
lookAtX += (direction * step);
D3DXMATRIX viewMatrix;
D3DXVECTOR3 viewEye(eyeX, eyeY, eyeZ);
D3DXVECTOR3 viewAt(lookAtX, lookAtY, lookAtZ);
D3DXVECTOR3 viewUp(0.0f, 1.0f, 0.0f);
D3DXMatrixLookAtLH(&viewMatrix, &viewEye, &viewAt, &viewUp);
D3DDevice->SetTransform(D3DTS_VIEW, &viewMatrix);
};
让摄像机跟随玩家移动,其实就是修改玩家位置的同时,同时调整摄像机的位置和观察点。接下来我们创建“PlayerClass.h”和“PlayerClass.cpp”两个文件,其中“PlayerClass.h”文件内容如下:
#pragma once
#include "CollideBox.h"
#include "CameraClass.h"
#include "SpriteClass.h"
// 玩家类,继承精灵类,拥有碰撞盒和摄像机类
class PlayerClass : public SpriteClass {
public:
int direction; // 移动方向(0不移动,1向右,-1向左)
float step; // 移动步长
bool isJump; // 是否跳跃
bool isJumpTop; // 是否到达最高点
float jumpMax; // 跳跃最高值
CameraClass* camera; // 摄像机
CollideBox* box; // 碰撞盒
bool isCollideStopLeftMove; // 碰撞停止向左移动
bool isCollideStopRightMove; // 碰撞停止向右移动
bool isCollideStopJump; // 碰撞停止跳跃
public:
// 构造函数,调用父类构造函数
PlayerClass(LPDIRECT3DDEVICE9 _device, CameraClass* _camera, float _x, float _y, float _w, float _h);
// 重写父类update函数
void update();
// 重载父类update函数
void update(bool isDown, char key);
// 解析动画纹理贴图索引cur
int getCur();
// 移动
void moveX();
// 跳跃
void jumpY();
// 碰撞检测回调
void collideCallback(std::wstring tag, CollidePositioin pos);
};
玩家类就稍微更复杂一下,它的移动是用户键盘输入操控的,另外还支持跳跃。请注意,这是两种截然不同的动画方式。移动是根据用户的不间断的键盘事件来驱动实现的,而跳跃则是根据动画轨迹参数实现的,键盘事件只是一个触发效果。这也是上文中提到的,update函数的执行,不一定必须由用户交互事件触发执行。具体的“PlayerClass.cpp”代码如下:
#include "PlayerClass.h"
// 玩家类: 构造函数,调用父类构造函数
PlayerClass::PlayerClass(LPDIRECT3DDEVICE9 _device, CameraClass* _camera, float _x, float _y, float _w, float _h) : SpriteClass(_device, _x, _y, _w, _h) {
// 赋值摄像机
camera = _camera;
// 计算碰撞盒
box = new CollideBox(vertexes, 6);
box->updateCollide(_x, _y);
// 初始化纹理贴图
std::wstring files[] = {
L"asset/mario_right_stand.bmp",
L"asset/mario_right_run1.bmp",
L"asset/mario_right_run2.bmp",
L"asset/mario_right_run3.bmp",
L"asset/mario_right_jump.bmp",
L"asset/mario_left_stand.bmp",
L"asset/mario_left_run1.bmp",
L"asset/mario_left_run2.bmp",
L"asset/mario_left_run3.bmp",
L"asset/mario_left_jump.bmp",
L"asset/mario_dead.bmp" };
int size = sizeof(files) / sizeof(files[0]);
load(files, size);
// 默认纹理索引
cur = 0;
// 默认绘制
isShow = true;
// 移动步长
direction = 0;
step = 2.0f;
// 跳跃最高值
jumpMax = 50;
isJump = false;
isJumpTop = false;
// 碰撞停止移动
isCollideStopLeftMove = false;
isCollideStopRightMove = false;
// 碰撞停止跳跃
isCollideStopJump = false;
};
// 玩家类: 重写父类update函数
void PlayerClass::update() {};
// 玩家类: 重载父类update函数
void PlayerClass::update(bool isDown, char key) {
// 玩家向左移动
if(key == 'L') {
if (isDown) direction = -1;
else direction = 0;
}
// 玩家向右移动
if (key == 'R') {
if (isDown) direction = 1;
else direction = 0;
}
// 玩家跳跃
if (key == 'S') {
if (isDown) isJump = true;
}
// 修改纹理贴图索引值(形成动画)
cur = getCur();
// 跳跃
jumpY();
// 移动
moveX();
};
// 玩家类: 解析动画纹理贴图索引cur
int PlayerClass::getCur() {
// 向右移动
if (direction == 1) {
// 跳跃(有点问题)
if (isJump) return 4;
// 转身
if (cur > 4 && cur < 10) return 0;
// 最大
if (cur > 3) return 1;
// 累加
return cur + 1;
}
// 向左移动
if (direction == -1) {
// 跳跃(有点问题)
if (isJump) return 9;
// 转身
if (cur < 5) return 5;
// 最大
if (cur > 8) return 6;
// 累加
return cur + 1;
}
// 保持不变
return cur;
};
// 玩家类: 移动
void PlayerClass::moveX() {
// 不移动
if (direction == 0) return;
// 碰撞对移动的影响
if (isCollideStopLeftMove && direction == -1) return;
if (isCollideStopRightMove && direction == 1) return;
// 修改玩家X轴坐标位置
x += (step * direction);
// 玩家不能出屏幕
if (x < 0) { x = 0; return; }
if (x > 710) { x = 710; return; }
// 更新碰撞盒(X轴)
box->updateCollide(true, step * direction);
// 摄像机向左移动限制
if (x < 150) return;
// 摄像机向右移动限制
if (x > 490) return;
// 修改摄像机位置
camera->update(direction, step);
};
// 玩家类: 跳跃
void PlayerClass::jumpY() {
// 没有跳跃
if (isJump == false) return;
// 碰撞对跳跃的影响
if (isCollideStopJump) return;
// 到达最高点
if (y >= jumpMax) {
y = jumpMax;
isJumpTop = true;
}
// 结束跳跃
if (isJumpTop && y <= 0) {
y = 0;
isJump = false;
isJumpTop = false;
return;
}
// 修改Y坐标值
int d = 1;
if (isJumpTop) d = -1;
y += step * d;
// 更新碰撞盒(Y轴)
box->updateCollide(false, step * d);
};
// 玩家类: 碰撞检测回调
void PlayerClass::collideCallback(std::wstring tag, CollidePositioin pos) {
// 如果遇到静态物体
if (tag == L"pipe"){
// 停止移动
if (pos == LEFT) isCollideStopLeftMove = true;
if (pos == RIGHT) isCollideStopRightMove = true;
// 停止跳跃
if (pos == UP || pos == DOWN) isCollideStopJump = true;
// 恢复移动或跳跃
if (pos == NO) {
if (isCollideStopLeftMove) isCollideStopLeftMove = false;
if (isCollideStopRightMove) isCollideStopRightMove = false;
if (isCollideStopJump) isCollideStopJump = false;
}
}
// 如果遇到怪物,且碰撞点在左边或者右边的话,玩家死亡
if (tag == L"monster" && (pos == LEFT || pos == RIGHT)) {
isShow = false;
}
};
首先是我们的构造函数,先调用父类的构造函数构造四边形,然后计算碰撞盒,紧接着创建纹理数组,一共11张图片,包括死亡,左右行走,以及跳跃的图片,如下:
在update函数中,我们将完成跳跃和移动,以及动画效果。在这个位置,我们遇到了问题。因为update函数没有参数,用户的键盘输入信息无法传递过去。正确的处理方式,应该是让用户的键盘信息独立到一个类中,然后可以直接通过该类来访问键盘信息。在本案例中,我们就不在处理这个问题,我们这里重载一个新update方法来完成这部分内容。重载的新方法,第一个参数是键盘是否按下,第二个参数就是键盘键值标识。getCur函数是调整纹理数组下标变量,这里代码比较凌乱,其实我们应该按照角色的动作来分组纹理图片。当我们的角色有很多的动作的时候,各个动作中间的切换是一件非常复杂的事情。游戏开发当中,会有行为树和状态机两个概念,用于解决这方面的问题。在这里,我们不在详细叙述了。接下来就是moveX方法,改变玩家的X坐标,同步碰撞盒以及正交摄像机跟随。跳跃Jump函数稍微复杂一下,它要检查是否开始跳跃,以及是否跳跃最高点等等。根据这些标识来调整玩家Y坐标,以及同步碰撞盒。最后的碰撞回调方法,如果与静态物体碰撞,则会影响玩家的移动和跳跃。如果与怪物碰撞,且碰撞位置是左右的话,玩家死亡。到此位置,我们所有需要封装的类都完成了。下面就是主源文件“main.cpp”的内容。按照我们上文中提到的,我们的游戏主体循环中最主要的就是先执行update函数,而后执行render函数。update函数用于响应用户的输入事件并对游戏数据的修改,而render函数则是根据游戏数据进行画面渲染。那么游戏中添加了碰撞之后,这部分代码应该放在什么位置呢?其实,碰撞的检测和回调处理,本质仍然是对游戏数据的处理,因此它与update是相似的。不同之处在于,update是响应用户输入事件,而碰撞检测则是游戏中的对象碰撞触发。这样看来,碰撞检测代码的最好位置应该是在render函数中renderScene函数的后面。也就是说当游戏画面渲染之后,就执行碰撞检测,检测完之后执行update函数,而后执行renderScene函数,然后再次执行碰撞检测,这样一致循环下去。为此,我们需要声明碰撞检测函数,并在renderScene函数调用处的后面调用该函数,如下:
// 声明碰撞检测函数
void collideCheck();
接下来,我们先是全局变量的声明,代码如下:
// Direct3D设备指针对象
LPDIRECT3DDEVICE9 D3DDevice = NULL;
// 鼠标位置
int mx = 0, my = 0;
// 摄像机类
CameraClass* camera;
// 地图类
MapClass* map;
// 玩家类
PlayerClass* mario;
// 静态物体类(管道)
StaticObjectClass* pipe;
// 怪物类
MonsterClass* mushroom;
接下来就是我们的initScene函数,代码如下:
// 初始化摄像机
float x = 200.0f;
float y = 90.0f;
float z = -10.0f;
camera = new CameraClass(D3DDevice, x, y, z, x, y, 0.0f, WINDOW_WIDTH/2, WINDOW_HEIGHT/2);
// 地图类
map = new MapClass(D3DDevice, L"asset/map.jpg");
// 静态物体类(管道)
pipe = new StaticObjectClass(D3DDevice, L"asset/pipe.bmp", 500.0f, 0.0f, 32.0f, 32.0f);
// 玩家类
mario = new PlayerClass(D3DDevice, camera, 0.0f, 0.0f, 20.0f, 20.0f);
// 怪物类
mushroom = new MonsterClass(D3DDevice, 300.0f, 0.0f, 20.0f, 20.0f);
// 线性纹理
D3DDevice->SetSamplerState(0, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR);
D3DDevice->SetSamplerState(0, D3DSAMP_MINFILTER, D3DTEXF_LINEAR);
// 初始化投影变换
initProjection();
// 初始化光照
initLight();
由于我们的摄像机已经包括了投影变换代码,因此initProjection函数只需要执行camera的init方法即可。initLight函数还是以前的旧代码,使用全局环境光照和默认材质。接下来就是render函数的调整,也就是在renderScene函数的后面添加collideCheck函数的调用。接下来就是renderScene函数,代码如下:
// 游戏对象渲染,注意顺序
map->render();
pipe->render();
mario->render();
mushroom->render();
在渲染游戏对象的过程中,这个顺序非常重要。因为这涉及到一个遮挡的问题。在同一位置上,后渲染的物体会遮挡先渲染的物体。有时候,为了能够统一管理游戏对象的前后关系,我们会将游戏对象分组进行渲染。在Unity中,可以自定义层,将游戏对象置于不同的层中,就能够达到先后渲染和遮挡的问题了。本案例暂不考虑这么复杂的涉及。运行代码,我们就能看到大致的场景了。如下图:
接下来就是我们的update函数,它是整个游戏的关键所在。它要做的事情就是调用游戏对象的update方法。当然,比较特殊的一个就是玩家类的update方法,我们得调用重载的那个,用于接收用户的键盘输入信息。代码如下:
// 调用怪物的update函数
mushroom->update();
// 用户输入信息处理
char key = NULL;
if (wParam == VK_LEFT) key = 'L';
if (wParam == VK_RIGHT) key = 'R';
if (wParam == VK_SPACE) key = 'S';
bool isDown = type == 1 ? true : false;
// 调用玩家的重载update函数
if(key != NULL) mario->update(isDown, key);
有了update的方法,怪物和玩家就可以移动了。剩下就是collideCheck函数的碰撞检测代码了,如下:
// 玩家与静态物体碰撞检测
CollidePositioin pos = mario->box->isCollideBox(pipe->box);
mario->collideCallback(L"pipe", pos);
// 玩家与怪物碰撞检测
pos = mario->box->isCollideBox(mushroom->box);
mario->collideCallback(L"monster", pos);
mushroom->collideCallback(L"player", pos);
// 怪物与静态物体碰撞检测
pos = mushroom->box->isCollideBox(pipe->box);
mushroom->collideCallback(L"pipe", pos);
至此,我们的游戏基本完毕了,后续的功能,有兴趣的同学可以自己添加。本案例的代码设计思路就是对游戏对象的封装,它完成了update,render以及碰撞检测的三个重要内容。这个也属于面型对象的编程思想。在本案例中,碰撞的复杂程度还在于,碰撞对移动和跳跃的影响。为了尽量降低collideCallback函数与moveX函数的耦合程度,我们使用了中间变量(isCollideStopLeftMove和isCollideStopRightMove)来完成碰撞对移动的影响。
关于碰撞的另一个案例就是“拾取”,在射线内容的时候,我们大致介绍过。这里我们只提供项目为“D3D_07_Pickup”,有兴趣的同学可以根据注释研究一下。由于篇幅原因,我们不再介绍这个项目。粒子系统:粒子系统用来模拟爆炸,烟火等效果,它是由许多小的图形组成,每个粒子就是一个小的图形,它包含位置,大小,颜色,移动等属性。一般情况下,我们都是用添加纹理的小正方形来模拟粒子。粒子系统的复杂程度主要体现在粒子的移动以及移动中的变化。因此,开发一套能够模拟大多数游戏特性的粒子系统是一件非常复杂的工作。本章节,我们来模拟一个简单爆炸的一个2D特效。项目名称为“D3D_07_Particle”。粒子系统虽然是通过小四边形来实现的,但是难点在于这些小粒子的运动,以及运行中的变化,这些都是通过数学计算来实现的,尤其是粒子的运行轨迹,是非常的复杂的。由于篇幅原因,我们不再介绍这个项目。
本课程的所有代码案例下载地址:
备注:这是我们游戏开发系列教程的第二个课程,这个课程主要使用C++语言和DirectX来讲解游戏开发中的一些基础理论知识。学习目标主要依理解理论知识为主,附带的C++代码能够看懂且运行成功即可,不要求我们使用DirectX来开发游戏。课程中如果有一些错误的地方,请大家留言指正,感激不尽!