Xcode与C++之游戏开发:Pong游戏
上一篇:Xcode与C++之游戏开发:2D图形
接下来在前两天游戏骨架的基础上实现一个经典的乒乓球(Pong)游戏。游戏是这样的,一个球在屏幕上移动,玩家控制球拍来击打球。可以说乒乓球游戏是游戏开发者的 “Hello World” 项目。
绘制游戏中的物体
乒乓球球拍我们使用矩形来表示。绘制填充矩形,SDL 有 SDL_RenderFillRect
函数,它接受一个 SDL_Rect
代表的填充矩形,而矩形颜色由当前的绘图颜色决定。换句说,现在我们不改变绘图颜色,它默认就会使用那个幸福浪漫的蒂芙尼蓝。当然,颜色一摸一样,我们是看不见的。为了看得出矩形,将它修改成蓝色。
在 Game::GenerateOutput()
交换缓冲区之前写入:
// 设置绘制颜色SDL_SetRenderDrawColor(mRenderer, 0, 0, 255, 255);
要绘制矩形,需要指定一个 SDL_Rect
结构体。这个结构体有4个参数,左上角的点x/y坐标,还有矩形的高和宽度。在绝大多数的图形库中,包括 SDL,窗口左上角的点的坐标是(0, 0),x正半轴是向右,y正半轴是向下的(和数学上的相反)。
假设我们要在屏幕上方绘制矩形作为游戏的墙,可以使用下面的 SDL_Rect
的定义:
// 顶部墙的参数SDL_Rect wall {0, // 左上 x 坐标0, // 左上 y 坐标1024, // 宽度kThickness // 高度};
宽度被硬编码成1024,一般来说这需要根据窗口尺寸自动修正,后面会考虑修正这个问题。kThickness
是一个 const int
常量,被设置成15,这是为了方便调整墙的厚度。
C++ 不推荐使用
#define
宏预定义,更推荐使用const
在头文件声明之后加入:
const int kThickness = 15;
最后,用 SDL_RenderFillRect
绘制矩形,传入 SDL_Rect
指针:
SDL_RenderFillRect(mRenderer, &wall);
这样游戏窗口上面多了一道墙,类似的,可以画出底部的墙和右边的墙,只需要通过改变 SDL_Rect
的参数。比如,下面那道墙左上角的 y 坐标就是 768 - kThickness
(因为窗口初始化时高度被初始化为768)。
// 绘制底部墙wall.y = 768 - kThickness;SDL_RenderFillRect(mRenderer, &wall);// 绘制右边的墙wall = {1024 - kThickness,0,kThickness,1024};SDL_RenderFillRect(mRenderer, &wall);
左边的墙呢?留着给玩家打乒乓球。
墙硬编码可能问题不大,乒乓球球和球拍就不能硬编码了。随着游戏循环,球拍是要会动的。实际游戏编程中,球和球拍应该抽象成类,但是现在我们姑且先用变量代替一下硬编码。
首先,先定义一个 Vector2
结构体来存储 x 和 y 坐标。这个定义放到 Game.hpp
之中:
// Vector2 结构体仅存储 x 和 y 坐标
struct Vector2
{float x;float y;
};
接着添加两个 Vector2
的成员变量到 Game
类中,一个作为球拍 mPaddlePos
,一个作为球 mBallPos
。
// 球拍位置Vector2 mPaddlePos;// 球的位置Vector2 mBallPos;
之后在初始化时 Game::Initialize()
赋予一个合理的初始值。
// 初始化球拍和球的坐标mPaddlePos.x = 10.0f;mPaddlePos.y = 768.0f / 2.0f;mBallPos.x = 1024.0f / 2.0f;mBallPos.y = 768.0f / 2.0f;
x和y在这里是中心坐标,不符合 SDL_Rect
的要求。因此要先把x和y坐标转换成左上角的点。
// 绘制球拍SDL_Rect paddle {static_cast<int>(mPaddlePos.x),static_cast<int>(mPaddlePos.y - kPaddleH / 2),kThickness,static_cast<int>(kPaddleH)};SDL_RenderFillRect(mRenderer, &paddle);// 绘制球SDL_Rect ball {static_cast<int>(mBallPos.x - kThickness / 2),static_cast<int>(mBallPos.y - kThickness / 2),kThickness,kThickness};SDL_RenderFillRect(mRenderer, &ball);
其中,kPaddleH
也是个 const int
常量,用来控制球拍的长度(可控制难度),将其设置为 100.0f
。
const float kPaddleH = 100.0f;
这样乒乓球的所需要物体,或者说简陋的素材就完成了。看看效果。
接下来,就要把静态变成动态,完成游戏循环和交互逻辑的编写。
更新游戏
在编写游戏循环的时候,有一点是值得特别关注的,就是时间。务必记住一点,游戏循环不是连续的,它只是刷新频率带来的错觉。游戏中的时间和现实的物理时间也未必一样。这是开发的时候需要特别注意的。
由时间引出了刷新频率的问题,假如最早的游戏在 8MHz 的处理器下运行,有这样的代码:
// x 坐标更新五个像素
enemy.mPosition.x += 5;
那么,如果在 16MHz 的处理器上运行呢?刷新的速率翻了一倍,会导致游戏速度快了一倍。游戏的难度陡然上升,完全有可能将困难的挑战变成不可能。为了解决这个问题,游戏将使用增量时间(delta time)——上一帧到现在的时间流逝长度。
要采用增量时间,就需要转换思考的角度,从每帧移动的像素数量转变成每秒移动的像素数量。所以,我们把速度调整为150每秒,采用增量时间,这样更加灵活:
// 每秒更新150个像素
enemy.mPosition.x += 150 * deltaTime;
现在,代码与帧速无关了,无论是 30 FPS 还是 60 FPS,都能照常运行。实际游戏编程时,大部分都需要采用增量时间。
为了计算增量时间,SDL 提供了 SDL_GetTicks
函数返回从 SDL_Init
调用以来的毫秒数。通过存储上一帧的 SDL_GetTicks
的结果在成员变量里,就可以使用现在的值来计算增量时间。
首先,声明一个 mTicksCount
作为 Game
的成员变量,并在构造函数中初始化为0。
在 Game.hpp
的 Game
中增加代码:
// 记录运行时间Uint32 mTicksCount;
构造函数中初始化为0:
Game::Game()
:mWindow(nullptr)
,mRenderer(nullptr)
,mIsRunning(true)
,mTicksCount(0)
{}
使用 SDL_GetTicks
的代码在 Game::UpdateGame
中实现:
void Game::UpdateGame()
{// 增量时间是上一帧到现在的时间差// (转换成秒)float deltaTime = (SDL_GetTicks() - mTicksCount) / 1000.0f;// 更新运行时间(为下一帧)mTicksCount = SDL_GetTicks();
}
仔细想想,这代码还是有问题的。有一些依赖于物理的游戏(比如平台跳跃类),行为会根据帧速率而有所不同。最简单的解决方案就是限制速度,强制游戏循环需要等到一个增量时间。例如,目标帧速 60 FPS,一帧完成仅需要 15ms,则强制附加 1.6ms。
SDL 已经为我们提供了限制帧速的方法。例如,要限制至少帧与帧之间间隔 16ms,可以在 UpdateGame
一开始附加上代码:
void Game::UpdateGame()
{// 等到与上一帧间隔 16mswhile (!SDL_TICKS_PASSED(SDL_GetTicks(), mTicksCount + 16));// 增量时间是上一帧到现在的时间差// (转换成秒)float deltaTime = (SDL_GetTicks() - mTicksCount) / 1000.0f;// 更新运行时间(为下一帧)mTicksCount = SDL_GetTicks();
}
除了关注最短间隔,我们也应该关注最长间隔。例如,在调试的时候中断了游戏,那么一段时间之后恢复运行,游戏将产生一个突跃。为了解决这个问题,只需要设定一个增量时间的最大值。
// 固定增量时间最大值if (deltaTime > 0.05f){deltaTime = 0.05f;}
球拍位置
在乒乓球游戏中,我们可以通过键盘输入 W
向上移动球拍,S
向下移动球拍。可以定义一个 mPaddleDir
表示方向,-1
表示球拍向上(负y),1
表示球拍向下移动。
// 球拍方向int mPaddleDir;
记得在构造函数中初始化,
Game::Game()
:mWindow(nullptr)
,mRenderer(nullptr)
,mIsRunning(true)
,mTicksCount(0)
,mPaddleDir(0)
{}
控制游戏的位置通过键盘输入,因此代码放到 ProcessInput
中。
// 通过 W/S 更新球拍位置mPaddleDir = 0;if (state[SDL_SCANCODE_W]){mPaddleDir -= 1;}if (state[SDL_SCANCODE_S]){mPaddleDir += 1;}
注意这里是加上和减去,而不是采取直接赋值为 -1 和 1,因为这样才能确保玩家同时按两个键时mPaddleDir
是0。接下来,在 UpdateGame
中根据增量时间和方向,更新球拍位置。除此之外,还需要防止球拍超出窗口,必须限制在有效范围内。
// 根据方向更新球拍位置if (mPaddleDir != 0){mPaddlePos.y += mPaddleDir * 300.0f * deltaTime;// 确保球拍不能移出窗口if (mPaddlePos.y < (kPaddleH / 2.0f + kThickness)){mPaddlePos.y = kPaddleH / 2.0f + kThickness;} else if (mPaddlePos.y > (768.0 - kPaddleH / 2.0f - kThickness)){mPaddlePos.y = 768.0f - kPaddleH / 2.0f - kThickness;}}
在这里,速度是300个像素每秒。现在,可以编译运行一下看看了。
更新球的位置
更新球的位置就有一点点复杂了。球拍仅仅在一维(y)上运动,而乒乓球可是在二维平面上运动。另外,球碰到墙和球拍会反弹,从而改变它的方向。因此,需要使用球的速度和对它进行碰撞检测。
由于球在二维平面上运动,因此对速度的表示采用的是矢量分解。添加一个 Vector2
成员变量 mBallVel
,初始化成 (-200.0f, 235.0f)
,代表一开始球向x负方向以每秒200像素移动,同时向下每秒移动235像素。换句话说,向左下移动。
// 球的速度Vector2 mBallVel;
在 Game::Initialize()
中进行初始化:
mBallVel = {-200.0f, 235.0f};
接下来,需要编写能够将球从墙上弹回的代码。用于确定球是否与墙壁碰撞的代码类似于检查球拍是否在屏幕外。如果球的y位置小于或等于球的高度,则球与顶壁碰撞。一个问题是,碰撞之后怎么运动。
应该不难想到,假如球从上到下,那么碰到底部的墙将反弹向上。类型的,碰撞到右边,则反弹向左。基于矢量的原理,仅仅只需要在相应的分量上乘以 -1,改变方向即可。
// 球是否和顶部墙相碰if (mBallPos.y <= kThickness && mBallVel.y < 0.0f){mBallVel.y *= -1;}else if (mBallPos.y >= (768 - kThickness) && mBallVel.y > 0.0f){// 球和底部墙相碰mBallVel.y *= -1;}
对速度进行校验是有必要的,否则可能导致球粘在墙上。
球与墙的碰撞稍微简单一点,与球拍的碰撞就比较复杂了。
- 如果球在球拍的正上方和正下方,这就是没碰撞了;(球的y坐标和球拍的y坐标差值大于球拍的高度的一半)
- 检查球的x坐标是否和球拍一致,并且球不处于远离状态。
// 是否和球拍相交float diff = mPaddlePos.y - mBallPos.y;// 取绝对值diff = (diff > 0.0f) ? diff : -diff;if (// y分量差距足够小diff <= kPaddleH / 2.0f &&// 球拍的x范围内mBallPos.x <= 25.0f && mBallPos.x >= 20.0f &&// 球正向左运动mBallVel.x < 0.0f){mBallVel.x *= -1.0f;}// 如果球出了窗口,结束游戏else if (mBallPos.x <= 0.0f){mIsRunning = false;}// 如果球碰到右边的墙,则反弹else if(mBallPos.x >= (1024.0f - kThickness) && mBallVel.x > 0.0f){mBallVel.x *= -1.0f;}
最终
这样,游戏届的“Hello World”的Pong游戏就编写完成了,最后,改一下窗口标题,换成 Pong
,就可以了。但是,我们把 Pong 游戏的各个游戏对象都放到了 Game
中,这不利于扩展。这就是我们之后将进一步讨论的游戏对象。
下一篇:Xcode与C++之游戏开发: 游戏对象
Xcode与C++之游戏开发:Pong游戏相关推荐
- 微信小游戏开发教程-游戏实现3
微信小游戏开发教程-游戏实现3 对象池 由于游戏过程中会创建很多临时对象,这些对象很快又不再使用,垃圾回收器也能帮我们主动回收这部分垃圾,但是回收时间不可控制,同时增大了创建对象的开销,所以我们使用对 ...
- 微信小游戏开发教程-游戏实现2
微信小游戏开发教程-游戏实现2 绘制地面 类似于绘制背景,读者自行完成代码.src/runtime/land.js 简易View系统 坐标布局对于复杂的页面来说维护相当困难,因此这里我们引入布局的概念 ...
- 微信小游戏开发教程-游戏实现1
微信小游戏开发教程-游戏实现1 概述 微信开发者工具官方提供一个飞机大战的游戏Demo,这里我们不再使用这个demo,我们以FlappyBird为例,为了让读者更加容易理解. 源码 https://g ...
- java演练 猜奇偶小游戏开发 DB游戏必输的设计
java演练 猜奇偶小游戏开发 DB游戏必输的设计 阶段一,视频 https://www.ixigua.com/6870390946270446088?logTag=J_BVJOm_LIpQ-hWYY ...
- android_Android游戏开发–基本游戏架构
android 因此,我们启动并运行了我们的Android应用程序,但是您可能想知道哪种类型的应用程序正是游戏. 我会尽力让您了解它. 下图显示了游戏架构. Android手机上的游戏架构 在上面的架 ...
- 游戏开发技术——游戏引擎
游戏开发技术--游戏引擎 是什么:游戏引擎是指一些已编写好的可编辑电脑游戏系统或者一些交互式实时图像应用程序的核心组件.这些系统为游戏设计者提供各种编写游戏所需的各种工具,其目的在于让游戏设计者能容易 ...
- android 开发游戏_Android游戏开发–基本游戏循环
android 开发游戏 在到目前为止的系列之后,您将对游戏架构有所了解. 即使只是短暂的一次,但我们知道我们需要以某种形式进行输入,更新游戏的内部状态,最后将其渲染到屏幕上,并产生一些声音和/或振动 ...
- Unity游戏开发之游戏存档方式
目录 1.Unity自带存储方式PlayerPrefs 2.XML存储方式 3.Json类型存储方式 1.Unity的序列化问题 2.Unity中支持序列化的类 3.Unity中Json的使用方法 4 ...
- android策略模式_Android游戏开发–设计游戏实体–策略模式
android策略模式 在这一部分中,我将尝试解释我对好的游戏设计元素的理解. 我将在示例中使用droid,并编写基本的战斗模拟器脚本以查看其行为. 问题: 我指挥一个机器人,我想消灭敌人. 再次面对 ...
- 雪碧图 游戏开发_Android游戏开发–雪碧动画
雪碧图 游戏开发 如果到目前为止您仍然关注该系列 ,我们将在处理触摸,显示图像和移动它们方面广为人知. 但是,动态图像看起来很呆板,因为它看起来确实是假的和业余的. 为了给角色一些生活,我们将需要做更 ...
最新文章
- 两款轻量级服务器 Http-server SimpleHTTPServer
- ab 发送post请求测试API性能
- 错误代码#1045 Access denied for user 'root'@'localhost' (using password:YES)
- Kali 2017更新源
- Linux启动管理:grub
- Lua-泛型for循环 pairs和ipairs的区别
- Mac 开发使用中的小技巧收集
- .Net基础篇_学习笔记_第六天_For循环语法
- jquery 表单 清空
- nginx/windows: nginx多虚拟主机配置
- 什么是像素格式(色彩采样、色度抽样)RGB 4:4:4、(Limit)RGB 4:4:4、Ycbcr 4:4:4、Ycbcr 4:2:2、Ycbcr 4:2:0又是什么?
- HTML5播放器应用
- 老徐小程序之小程序怎么选?
- Python-从txt中获取所有带有书名号的内容,并去除重复内容
- IOS版本回退操作教程
- sharemouse切窗口就锁定了什么原因_使各大网课软件监控功能和锁定功能“失效”...
- CSDN博客个人账号注册与登录
- 云计算已渗透大众生活,去中心化云计算发展前景广阔
- bmi计算 python_python tkinter bmi计算
- 2022 Pwnhub冬季赛 WP
热门文章
- [附源码]Java计算机毕业设计SSM党史知识竞赛系统
- Linux 各个发行版本的发展史
- QMS-企业数字化转型-为什么中小型企业更应该导入质量管理软件以及比大企业更容易实现数字化转型?
- Linux学习路线(尚观)
- LG P990 (LG Optimus擎天柱2X) 获得Root权限的方法
- 【BUG日记】【JS】出现Cannot read property ‘xxx‘ of null
- XSS闯关小游戏通关笔记
- XMind课堂之思维导图学习法
- All of a Sudden
- 小程序中读取腾讯文档的表格数据