C++: 项目实战
- TAGS: C++
这个项目主要是《C++入门到精通》学完以后,我们来验证下,到底学了什么东西,并且做一些简单的实战。会有一些数学知识,那么我们来实现一个3D赛车小游戏,大纲是这样的。
3D赛车
- 代码环境
- 编码环境 visual studio
- 编码插件 fitten code
- 游戏引擎 SFML
- 轨道绘制
- 梯形绘制
- 轨道绘制
- 边缘绘制
- 草坪绘制
- 互动功能
- 前后移动
- 左右移动
- 引入速度
- 跳跃功能
- 轨道进阶
- 曲线轨道
- 山坡轨道
- 循环轨道
- 轨道背景
- 轨道物品
- 物品绘制
- 数字路障
- 分数计算
- 重新生成
- UI界面
- 分数展示
- 播放音乐
- 音效播放
- 音乐播放
- 音乐切换
环境安装
代码插件
点击这里的扩展 -> 管理扩展。 点击安装。这里会显示 已计划更改。需要关闭时,开始修改。
无脑关闭 Visual Studio 2022。跳出这个框,点击左边的按钮安装。
渲染引擎
首先要下载 SFML:https://www.sfml-dev.org/download/sfml/2.6.1/ ,解压到本地放代码的文件夹。
然后创建一个项目,项目创建完以后,在 main.cpp 中敲入第一句代码,Graphics.hpp 这个头文件通常包含了与 *图形绘制、窗口管理 * 等相关的函数声明 以及 类的定义。通过包含这个头文件,程序就能够使用 SFML 库提供的图形处理功能,具体哪些功能呢?比如创建窗口、绘制图形、处理输入等等操作。
#include <SFML/Graphics.hpp>
这时候我们发现,无法打开这个文件。
因为需要进行简单的配置,点击项目,右键属性,选择这里的 C/C++,选择【附加包含目录】,选择 SFML 的 include 目录,点击确定。
然后点击 附加库 -> 常规,附加库目录,选择 SFML 的 lib 目录:
再点击下面的【输入】,选择右边的【附加依赖项】,把这些内容拷贝进去:
(这里需要注意,对于 sfml-xxx-d.lib 是适用于 debug 模式的,sfml-xxx.lib 是适用于 release 模式的)
sfml-graphics-d.lib sfml-window-d.lib sfml-system-d.lib sfml-audio-d.lib opengl32.lib freetype.lib winmm.lib gdi32.lib
最后点击应用,就配置完毕了。
然后再把 SFML-2.6.0\bin 目录下的 动态链接库文件 dll 都拷贝到项目目录下。
然后我们写个 main 函数来测试一下。
#include <SFML/Graphics.hpp> using namespace sf;
const int WinWidth = 1024; const int WinHeight = 768;
int main() { RenderWindow win(VideoMode(WinWidth, WinHeight), "Racing"); win.setFramerateLimit(60); // 设置帧率为 60 帧 while (win.isOpen()) {
} return 0; }
这样一来,就可以显示一个基本的窗口了,接下来我们来解释解释这些代码的含义。
这时候,当我们点击窗口的时候,会出现卡顿,并且未响应,这个原因是什么呢? 原因是我们没有处理事件,首先定义一个事件的类,然后调用 pollEvent 这个接口,就可以获取到事件队列里,最先来的那个事件,包括 鼠标事件、键盘事件 等等。
那么再学完数据结构以后,你就能理解队列的概念了,就是一种先进先出的数据结构。
可以通过获取事件的类型 type ,知道这是一个什么样的事件,比如 Event::Closed 代表窗口关闭事件,也就是你点了右上角的关闭按钮。当然,F12进去以后,可以看到更多事件的定义。
可以在控制台里输出一句话,来印证我们的观点,调用 win.close() 以后,win 的 isOpen 接口就会返回假,这样程序也就结束了。运行一下。
while (win.isOpen()) { Event e; while (win.pollEvent(e)) { if (e.type == Event::Closed) { win.close(); } } }
梯形绘制
接下来我们来尝试绘制一个梯形,为什么要绘制梯形呢?继续往下听,你就明白了。
这个梯形的坐标是这样的,横向是 x 轴,竖向是 y 轴,和我们传统的 xy 坐标系 是一致的。
实现一个 DrawTrape 函数,RenderWindow 就是这个 win,通过引用的方式传参传进来,Color 是一个颜色值,(x1,y1) 代表梯形下底的中点坐标,w1 代表下底的长度;(x2,y2) 代表梯形上底的中点坐标,w2 代表上底的长度。
这个函数的实现,就是先创建一个多边形 ConvexShape ,多边形的顶点数为 4,把这个多边形的颜色填充一下,填充参数是传参传进来的,然后按照这个图,设置四个顶点的坐标位置。第 0 个顶点,第 1 个顶点,第 2 个顶点,第 3 个顶点,最后调用 draw 接口,把它绘制出来。
void DrawTrape(RenderWindow& w, Color c, int x1, int y1, int w1, int x2, int y2, int w2) { ConvexShape polygon(4); polygon.setFillColor(c); polygon.setPoint(0, Vector2f(x1 - w1, y1) ); polygon.setPoint(1, Vector2f(x2 - w2, y2)); polygon.setPoint(2, Vector2f(x2 + w2, y2)); polygon.setPoint(3, Vector2f(x1 + w1, y1)); w.draw(polygon); }
实现完毕以后,就可以调用它了,先把窗口清理一下,然后调用 DrawTrape 这个函数,绘制的窗口传进去,颜色设置成白色,x1 的坐标设置为窗口的一半,y1 的坐标设置为 500,下底边长为 200;x2 的坐标也是窗口的一半,y2 的坐标设置为 300,上底边成为 100。
if (e.type == Event::Closed) { win.close(); } } win.clear(); DrawTrape(win, Color::White, WinWidth / 2, 500, 200, WinWidth / 2, 300, 100); win.display();
轨道绘制
这堂课我们通过透视原理,来绘制赛车的轨道,轨道宽度定义成 1800,每个轨道的这一段长度,定义成 180,总共定义 2000 个轨道。
const int roadWidth = 1800; const int roadSegLength = 180; const int roadCount = 2000;
对于每个轨道,用一个结构体来实现,小写的 x,y,z 代表,每个轨道的中心,在 3D 世界中的坐标,大写的 X 和 Y,则是 3D 映射到 2D 时的屏幕坐标,大写的 W 代表这条轨道映射到屏幕后,它的宽度。
实现一个带参构造函数,利用初始化列表进行初始化。
然后实现一个 project 函数,代表 3D 到 2D 的映射,camX,camY,camZ 代表的是 3D 相机的位置,接下来我会讲一些数学的概念。听不懂没有关系,只要你能够自己跟着把代码写出来,并且通过修改每个参数,运行后得到你想要的效果就可以了。不用太计较这里的数学计算。
首先,当相机位置 camZ 确定时,z 越大,那么这条轨道就应该越小,所以实现这么一个反比例函数,计算缩放比例 scale。
利用这个缩放比例,可以计算出 X 和 Y,像这样。
最后计算 W,也就是每个轨道的实际屏幕宽度,和 scale 强相关,所以作为系数乘上去。
struct Road { float x, y, z; float X, Y, W; float scale;
Road(int _x, int _y, int _z): x(_x), y(_y), z(_z) {}
void project(int camX, int camY, int camZ) { scale = 1.0 / (z - camZ); X = (1 + (x - camX) * scale) * WinWidth / 2; Y = (1 - (y - camY) * scale) * WinHeight / 2; W = scale * roadWidth * WinWidth / 2; } };
定义一个轨道的 vector,每个轨道的 x 和 y 都是 0,z 坐标按照 i 进行平铺。
vector<Road> roads; for (int i = 0; i < roadCount; ++i) { Road r(0, 0, (i+1) * roadSegLength); roads.push_back(r); }
因为近大远小的关系,所以屏幕上只显示 300 个轨道,对于每个轨道,调用 project 计算投影,传入的是相机的位置,其中 y 代表竖直方向,设定为 1600。
获取当前轨道和前一个轨道,分别为 now 和 pre,利用这两个轨道,可以计算出一个梯形,并且设置好颜色,稍微做点差异化,产生相间的效果。运行看下效果。
for (int i = 0; i < 300; ++i) { Road& now = roads[i % roadCount]; now.project(0, 1600, 0); if (!i) { continue; } Road& pre = roads[(i - 1) % roadCount]; Color road = i % 2 ? Color(105, 105, 105) : Color(101, 101, 101); DrawTrape(win, road, pre.X, pre.Y, pre.W, now.X, now.Y, now.W); }
当然,可以在 i 这里除上一个数,使得两个颜色区分的时候,不会那么密集。看下效果。这里如果你不理解,怎么办呢?你就多试几个数字,试着试着感觉就有了。
Color road = (i/3) % 2 ? Color(105, 105, 105) : Color(101, 101, 101);
边缘绘制
接下来,如法炮制,一般轨道的边上,会有一些边缘,这些边缘是黑白相间的,所以先计算边缘的颜色,根据模 2 的结果,要么是黑色,要么是白色。
Color edge = (i/3) % 2 ? Color(0, 0, 0) : Color(255, 255, 255);
然后长度比轨道稍微长一点,所以在 W 上,再乘上一个比例,比如 1.3 ,这个系数你也可以改成 1.4,1.5,然后再去看效果。运行一下。
DrawTrape(win, edge, pre.X, pre.Y, pre.W*1.3, now.X, now.Y, now.W*1.3);
草坪绘制
还是如法炮制,在最下面一层,放上草地,看起来就很完美了。设置草地的颜色。
Color grass = i / 3 % 2 ? Color(16, 210, 16) : Color(0, 199, 0);
同样是绘制梯形,但是绘制得大一点,把传进去的 W 参数设的大一点
DrawTrape(win, grass, pre.X, pre.Y, WinWidth, now.X, now.Y, WinWidth);
前后移动
定义一个变量 cameraZ ,然后当键盘上的 "上键" 按下的时候,cameraZ 加上一个 roadSegLength;相当于往前走了一个轨道,如果当键盘上的 "下键" 按下的时候,减去一个 roadSegLength,相当于往后走了一个轨道。
roadIndex 就是第一个要显示的轨道。还是显示 300 个轨道,可以这么去实现。
for (int i = 0; i < roadCount; ++i) { Road r(0, 0, (i+1) * roadSegLength); roads.push_back(r); } int cameraZ = 0;
while (win.isOpen()) { … if (Keyboard::isKeyPressed(Keyboard::Up)) cameraZ += roadSegLength; if (Keyboard::isKeyPressed(Keyboard::Down)) cameraZ -= roadSegLength;
win.clear(); int roadIndex = cameraZ / roadSegLength;
for (int i = roadIndex; i < roadIndex + 300; ++i) { Road& now = roads[i % roadCount]; now.project(0, 1600, 0);
哦哦哦哦哦,忘记让相机跟着往前走了。
now.project(0, 1600, cameraZ);
左右移动
除了前后移动,左右也可以移动。同样是控制相机的坐标,定义 cameraX 代表相机的 x坐标。
int cameraZ = 0; int cameraX = 0;
分别按下左键和右键时,更改 cameraX 的值。
if (Keyboard::isKeyPressed(Keyboard::Left)) cameraX -= 100; if (Keyboard::isKeyPressed(Keyboard::Right)) cameraX += 100;
然后在这里把 cameraX 代替原来 0 的值。
now.project(cameraX, 1600, cameraZ);
运行,发现露馅了。
没事,宽度乘10就好了。
DrawTrape(win, grass, pre.X, pre.Y, 10*WinWidth, now.X, now.Y, 10*WinWidth);
曲线轨道
这节课我们想办法让这条路,能够变成弯的,看起来更加的真实,在原来的路上,加上一个曲线因子。
float scale, curve; Road(int _x, int _y, int _z, float _c): x(_x), y(_y), z(_z), curve(_c) {}
然后在实例化 Road 的时候,当 i 在 0 到 300 之间,曲线因子就是 0.5,否则就是负的 0.5。构造的时候,传参传进去。
float curve = (i > 0 && i < 300) ? 0.5 : -0.5; Road r(0, 0, (i+1) * roadSegLength, curve);
然后定义一个 x 和 dx,初始化都为 0,这段代码怎么去理解呢?这里的 now.curve 就理解成加速度,那么 dx 就是速度,而 x 就是位移,这样这条路弯曲起来就看起来非常的连续了,看下效果。
int roadIndex = cameraZ / roadSegLength; float x = 0, dx = 0; for (int i = roadIndex; i < roadIndex + 300; ++i) { Road& now = roads[i % roadCount]; now.project(cameraX - x, 1600, cameraZ); x += dx; dx += now.curve;
山坡轨道
然后我希望这个轨道,有一种跌宕起伏的感觉,三角函数可以做到这一点,所以每个轨道的中心的 y 坐标,用一个三角函数 sin 来实现,振幅 1600,像这样:
Road r(0, sin(i/30.0)*1600, (i + 1) * roadSegLength, curve);
相机的位置从 1600 作为基准,再加上起始轨道的 y 坐标,像这样。
int roadIndex = cameraZ / roadSegLength; float x = 0, dx = 0; int cameraY = 1600 + roads[roadIndex].y; int minY = WinHeight;
这里的 1600 就可以改成这个 cameraY 了。所以这个 cameraY 的范围,就根据正弦函数的值,变成了 0 到 3200。
now.project(cameraX - x, cameraY, cameraZ);
这时候我们发现,效果好像不对,问题出在哪里呢?
原因是这样的。
需要注意的是,一旦出现跌宕起伏,那么如果出现一个 Y 值最小的轨道,后面比 Y 值大的轨道,就不应该被绘制出来,注意这里用的屏幕坐标系,所以越往上值越小。
int roadIndex = cameraZ / roadSegLength; float x = 0, dx = 0; int cameraY = 1600 + roads[roadIndex].y; int minY = WinHeight;
那么判断当前的 Y 是否比最小的 Y 小,如果小,那么记录这个最小值;否则直接 continue,代表这条轨道不用绘制,来看看效果。
if (now.Y < minY) { minY = now.Y; } else { continue; }
循环轨道
然后我们希望整条路,能够产生循环,这样就可以,用有限的 Road 对象来展现无限的路。
计算这条路的总长度 totalLength,如果发现 cameraZ 的位置已经大于总长度了,就减去总长度,如果小于总长度,就变成负数了,那么加上总长度。
其实就是让 cameraZ 的值,始终保持在 [0, totalLength) 之间。
int totalLength = roadCount * roadSegLength; while (cameraZ >= totalLength) cameraZ -= totalLength; while (cameraZ < 0) cameraZ += totalLength;
这里做一点修正。
now.project(cameraX - x, cameraY, cameraZ - (i >= roadCount ? totalLength : 0) );
然后你会发现,这里出现了一些断层,这个不是很合理。
根据 PI 计算一下,roadCount 让它是 3.14 的倍数就好了,那就 3.14 乘上 600,变成 1884 ,看下效果。
const int roadCount = 1884;
背景展示
准备一张云的图片,加载一个纹理,并且生成一个精灵。宽度是窗口宽度,高度是窗口高度的一半。
Texture bg; bg.loadFromFile("cloud.png"); Sprite s(bg, IntRect(0, 0, WinWidth, WinHeight/2));
然后在每次绘制轨道的时候,把它绘制出来。
if (now.Y < minY) { minY = now.Y; } else { continue; } win.draw(s); Road& pre = roads[(i - 1) % roadCount];
额……这样太假了
稍微改一下,把高度设置为 minY 就好了,这样就和轨道完全贴合了。
s.setTextureRect(IntRect(0, 0, WinWidth, minY));
引入速度
引入一个赛车前进的速度,speed 初始化为 0。
int cameraZ = 0; int cameraX = 0; int speed = 100;
当键盘按下 UP 和 DOWN 的时候,不再修改 cameraZ 的值,而是修改速度的值,然后最终,cameraZ 把这个速度累加上就好了。
这样一来,往前往后,修改的就是速度,不再是位移。
if (Keyboard::isKeyPressed(Keyboard::Up)) { speed += 2; if (speed > 1000) speed = 1000; } if (Keyboard::isKeyPressed(Keyboard::Down)) { speed -= 2; if (speed < 100) speed = 100; } cameraZ += speed;
物品绘制
接下来在这条路上,间隔的绘制物品,每个物品在图片的宽高是 450,这个作为常量,根据你图片的大小,进行修改即可。
const int itemSize = 450;
Road 的结构体中,定义一个精灵 spr。
struct Road { float x, y, z; float X, Y, W; float scale, curve; Sprite spr;
实现一个 drawItem 的函数,设置好纹理的矩形,缩放比例,以及位置,位置的计算方式比较简单,X 坐标和这条路保持一致,Y 坐标减去 W。然后给它绘制出来。
void drawItem(RenderWindow& win) { Sprite s = spr; s.setTextureRect(IntRect(0, 0, itemSize, itemSize)); s.setScale(W / itemSize, W / itemSize); s.setPosition(X, Y - W); win.draw(s); }
然后定义一个物品的纹理对象,加载对应的图片,生成一个精灵对象。
Texture item; item.loadFromFile("item.png"); Sprite sitem(item);
把精灵对象通过传参,传到 Road 对象中。
Road r(0, sin(i/30.0)*1600, (i + 1) * roadSegLength, curve, sitem);
然后一个 for 循环,注意从后往前绘制,否则遮挡关系会不对。为了不产生密集恐惧症,我们可以 % 上一个 20,每 20 个轨道,出现一个物品。
for (int i = roadIndex + 300; i > roadIndex; –i) { if (i % 20 == 0) roads[i % roadCount].drawItem(win); }
(哎哎哎哎哎……)
这里显示有点问题
毕竟是2D模拟3D
这里遮挡出问题了
如果这条路是平的
是不是就没问题了,那我们就把 跌宕起伏 的功能给它去掉就完事了。
Road r(0, 0, (i + 1) * roadSegLength, curve, sitem);
数字路障
这节课,我们把路上的这些物品,显示成一些不同的数字,有数字,有符号。
首先定义这么一个字符串,和这张图片中的每个图片一一对应。
const char charItem[] = "1234567890+*/-%";
然后在 Road 中定义两个变量,operatorIndex 代表符号在 charItem 中的下标,numberIndex 代表数字 charItem 中的下标。
int operatorIndex; int numberIndex;
接下来就可以开始初始化了,为了数字不会太密集,采用随机数的方式,如果是 200 的倍数,才出现物品。同样是通过随机的方式,尽量不出现 0,因为 除 0 和 模 0 都没意义。
operatorIndex 等于 -1 代表这个 Road 上没有东西。
Road(int _x, int _y, int _z, float _c, Sprite _spr): x(_x), y(_y), z(_z), curve(_c), spr(_spr){
if (rand() % 200 = 0) {
operatorIndex = (rand() % 5) + 10;
numberIndex = rand() % 10;
if (numberIndex = 9) {
numberIndex = 0;
}
}
else {
operatorIndex = -1;
}
}
然后我们重载原先的 drawItem,并且加入一个新的参数 ,index 代表的是 charItem 中的下标,xPlacement 则表示 x 方向的偏移。然后 left 和 top 则代表的是在这张图上的(对应数字或者符号)的左上角坐标。
void drawItem(RenderWindow& win, int index, int xPlacement) { Sprite s = spr; int left = (index % 5) * itemSize; int top = (index / 5) * itemSize; s.setTextureRect(IntRect(left, top, itemSize, itemSize)); s.setScale(W / itemSize, W / itemSize); s.setPosition(X + xPlacement*W, Y - W); win.draw(s); }
void drawItem(RenderWindow& win) { if (operatorIndex == -1) { return; } drawItem(win, operatorIndex, -1); drawItem(win, numberIndex, 0); }
这里的取模就可以去掉了。
for (int i = roadIndex + 300; i > roadIndex; –i) { roads[i % roadCount].drawItem(win); }
分数展示
这节课,我们在窗口的左上角,展示一个分数,并且还是这张图片。先定义一个分数 score,尽量考虑到所有情况,所以初始化 - 的 1234567890。
int score = -1234567890;
然后实现一个 DrawNumber 函数,sItem 代表数字对应那个精灵,x 和 y 代表显示在屏幕的左上角坐标。然后把 number 转换成字符串,存储到 ch 中。
遍历这个字符串,并且去 charItem 中找到对应的下标,利用相同的绘制方式,把它绘制在屏幕上。
void DrawNumber(RenderWindow& win, Sprite sItem, int number, int x, int y) { char ch[100] = {'\0'}; _itoa_s(number, ch, 10); int len = strlen(ch); for (int i = 0; i < len; ++i) { Sprite s = sItem; int index = -1; for (int j = 0; charItem[j]; ++j) { if (charItem[j] == ch[i]) { index = j; break; } } int left = (index % 5) * itemSize; int top = (index / 5) * itemSize; s.setTextureRect(IntRect(left, top, itemSize, itemSize)); s.setScale(0.18, 0.18); s.setPosition(x + 0.13 * itemSize * i, y); win.draw(s); } }
然后调用即可。
s.setTextureRect(IntRect(0, 0, WinWidth, minY)); win.draw(s); DrawNumber(win, sitem, score, 10, 10);
分数计算
先把分数重置了。
int score = 0;
实现一个计算分数的函数,计算方式很简单,传进来的是符号加数字,所以最终就是用当前分数,和这个传进来的数字,进行四则运算,分别处理 + - * / % 的情况即可。
int caculateScore(int score, int operatorIndex, int numberIndex) {
char op = charItem[operatorIndex];
int number = charItem[numberIndex] - '0';
if (op = '+') {
return score + number;
}
else if (op = '-') {
return score - number;
}
else if (op = '*') {
return score * number;
}
else if (op = '/') {
return score / number;
}
return score % number;
}
当 now.Y 大于等于窗口高度时,代表这个数字正好从我们身边 "溜走",所以如果这条轨道是有数字的,那么就调用 caculateScore,并且把这条路的数字清空即可。
if (!i) { continue; } if (now.Y >= WinHeight) { if (now.operatorIndex != -1) { score = caculateScore(score, now.operatorIndex, now.numberIndex); now.operatorIndex = -1; } }
重新生成
如果每次路过都把数字清空,那么迟早有一天,路上就没有数字了,于是实现一个重新生成的接口,这里做一点小改动,这个 Road 一开始,是通过随机计算的。
当然,如果 generateItem 直接传一个 true,那么必定产生数字。
Road(int _x, int _y, int _z, float _c, Sprite _spr): x(_x), y(_y), z(_z), curve(_c), spr(_spr){ generateItem(false); }
void generateItem(bool bAlwaysGen) {
if (bAlwaysGen || rand() % 200 = 0) {
operatorIndex = (rand() % 5) + 10;
numberIndex = rand() % 10;
if (numberIndex = 9) {
numberIndex = 0;
}
}
else {
operatorIndex = -1;
}
}
所以当前这条路如果正好出屏幕外,那么就把 i + 1500 的轨道,生成一个新的数字。
now.operatorIndex = -1; roads[(i + 1500) % roadCount].generateItem(true);
跳跃功能
为了增加趣味性,可以引入跳跃功能,从而可以跳过这个数字。加入一个状态叫 isJumping ,一开始是 false,代表没有跳跃。
跳跃会改变 z 的坐标,所以 z 代表实际偏移的 z ,dz 代表跳跃时的速度。
bool isJumping = false; float z = 0, dz = 0;
如果按下空格,当非跳跃状态,则切换到跳跃状态,并且把起跳速度设置为 150。
if (Keyboard::isKeyPressed(Keyboard::Space)) { if (isJumping == false) { isJumping = true; dz = 150; } }
然后如果一直在跳跃中,则减去 5,这里就是在模拟自由落体,这里的 5 就是加速度,然后 z 累加 dz 的过程,就是模拟的 位移 和 速度,当 z 小于等于 0,说明落地了,isJumping 置为 false。
if (isJumping) { dz -= 5; z += dz; if (z <= 0) { z = 0; isJumping = false; } }
最后,当遇到数字的时候,如果非跳跃状态,才会生效,否则没有任何效果。
if (now.Y >= WinHeight) { if (!isJumping && now.operatorIndex != -1) { score = caculateScore(score, now.operatorIndex, now.numberIndex); now.operatorIndex = -1; roads[(i + 1500) % roadCount].generateItem(true); } }
音效播放
接下来,我们加入一些音效,让游戏更加的带感。引入一个头文件。
#include <SFML/Audio.hpp>
初始化三个音效缓存,分别代表 碰到数字、起跳、落地 音效。
SoundBuffer buffer[3]; Sound sound; buffer[0].loadFromFile("get.mp3"); buffer[1].loadFromFile("jump.mp3"); buffer[2].loadFromFile("falldown.mp3");
起跳可以这么写。
if (isJumping == false) { sound.setBuffer(buffer[1]); sound.play(); isJumping = true; dz = 150; }
落地可以这么写。
if (z <= 0) { z = 0; isJumping = false; sound.setBuffer(buffer[2]); sound.play(); }
碰到数字可以这么写。
if (!isJumping && now.operatorIndex != -1) { score = caculateScore(score, now.operatorIndex, now.numberIndex); now.operatorIndex = -1; roads[(i + 1500) % roadCount].generateItem(true); sound.setBuffer(buffer[0]); sound.play(); }
音乐播放
再加上一个 bgm。
SoundBuffer buffer[5]; Sound sound, bgm; buffer[0].loadFromFile("get.mp3"); buffer[1].loadFromFile("jump.mp3"); buffer[2].loadFromFile("falldown.mp3"); buffer[3].loadFromFile("tianfuyue.mp3"); buffer[4].loadFromFile("liumaishenjian.mp3");
在窗口 while 之前,调用 play 接口。
bgm.setBuffer(buffer[3]); bgm.setLoop(true); bgm.play();
音乐切换
音乐可以随着赛车的速度,进行切换,当开的快的时候,播放 4 号 音乐;否则,播放 5 号音乐。
if (Keyboard::isKeyPressed(Keyboard::Up)) {
speed += 2;
if (speed > 1000) speed = 1000;
if (speed = 500) {
speed = 502;
bgm.setBuffer(buffer[4]);
bgm.play();
}
}
if (Keyboard::isKeyPressed(Keyboard::Down)) {
speed - 2;
if (speed < 100) speed = 100;
if (speed == 500) {
speed = 498;
bgm.setBuffer(buffer[3]);
bgm.play();
}
}