大话游戏循环Game Loop——PythonC++

news2024/11/27 9:39:04

目录

前言

什么是游戏循环?

游戏循环的意义

从结构上来看:

从功能上来看:

正文

一个简单的游戏循环

阻塞游戏循环

现代基础游戏循环

“时间”

固定帧率游戏循环

时间驱动不固定帧率游戏循环

灵活帧率更新

之后的新问题

总结

一些设计决策

谁来控制游戏循环?

能量损耗


14天学习训练营导师课程:
李宁《Python Pygame游戏开发入门与实战》
李宁《计算机视觉OpenCV Python项目实战》1
李宁《计算机视觉OpenCV Python项目实战》2
李宁《计算机视觉OpenCV Python项目实战》3


前言

什么是游戏循环?

游戏循环对于我们来说很常见了,就是所有游戏里面都有的一个东西。如果真要解释一下的话,就是让我们的CPU和GPU持续不断地更新游戏里面的内容并且将其渲染到屏幕上。相比于其他有交互的软件的循环,游戏内的循环逻辑是更难的,因为我们的目标是:让游戏不断地更新,并且在“获取输入”、“计算”、“渲染”这三个步骤都不造成大量的阻塞。那么知道这样的话,就让我们从游戏之“创世之初”——像先人一样思考游戏循环的有关始末吧!

PS:由于现在的很多游戏都是直接套用的游戏引擎,所以看起来游戏循环这个模式并不是那么必须了,因为有引擎来帮助我们考虑这些问题。但是我们都不能保证如果有一天我们自己的游戏项目需要用到自己的引擎,总之还是略微了解一点比较好,因此写作本文。


游戏循环的意义

游戏大体可以拆分为两大块:

从结构上来看:

游戏循环的开始就意味着游戏的开始。


从功能上来看:

游戏循环的作用

正文

一个简单的游戏循环

#创建游戏主窗口
screen = pygame.display.set_mode((480,700))

#游戏循环
while True:
    pass

PS:游戏窗口无需重复创建


阻塞游戏循环

关于这种游戏循环我并没有找到一个官方的名字,但是由于它会对线程进行阻塞,所以我就将其称为此名。我相信我们小时候没少想过这种场景:

// 在电脑上的例子
> 你走到了一个老房子的门口,老房子看起来很破旧,门半遮半掩,等待着你的进入...
> E: 进入;L:离开
> E
> 你进入了老房子......

或是在纸上画一个地图去找小伙伴,他们会将他们的行为告诉你,然后你按照产生的想法来进行下一步,这就是最简单的游戏逻辑的计算。


以上两种方式都是一样的,和传统的软件并没有什么区别,在游戏循环上都会被用户的输入而阻塞。比如说上面命令行的文字游戏,当它提示让玩家输入的时候,你如果不输入东西的话,它就会一直保持不动,直到接收到玩家的指令。

这种最基础的游戏循环用代码来实现是这样的:

bool isRunning = true;
while (isRunning)
{
    char* command = getCommand(); // 请求玩家输入并阻塞线程
    handleCommand(command);
}

PS:这对于那些没有动画、没有其他东西的文字游戏来说是很棒的方式,因为它足够简单。但是如果将其运用到我们现在的游戏上呢?除非你想搞点新花样,否则这就是没有意义的。因为现在的游戏,即使你在挂机也需要播放动画、音效、音乐、渲染等等。那么我们就需要针对于现代游戏的基础游戏循环了。


现代基础游戏循环

这种游戏循环也很简单,不过多赘述了,用代码实现是这样的:

bool isRunning = true;
while (isRunning)
{
    processInput(); // 不阻塞线程,即使没有输入也继续。
    update();
    render();
}

这样的话是不是看起来很棒了?即使没有用户的输入,游戏也正在一刻不停的运行——执行你在所有update里面写进去的逻辑。

这当然很棒,但是在这里有一个很容易发现的致命问题——时间。


“时间”

对于上面的代码而言,我们无时无刻在压榨着电脑的各种资源,我们的代码一刻不停的运行,之后就会出现这种场面。

假设A电脑每秒可以执行200次这样的循环,B电脑每秒可以执行30次这样的循环。那么,A电脑每秒会执行200次的Update(),而B电脑每秒只会执行30次。这就导致了在A电脑和B电脑上,游戏运行的速度不一样。比如说这个操作:“玩家按住W的时候,每一个Update向前移动0.5Unit的距离”。之后,在A电脑上每秒玩家会移动100Unit,在B电脑上只会移动15Unit。但是游戏的地图是不变的,于是就会有在两台机器上游戏体验不同的问题。(如果你用过变速齿轮的话,相信你一定可以很清晰的理解这段话的意思)。但是我们不想要这个问题出现,我们想让它在每台电脑上都运行都是差不多的。

所以从这里开始,我们引入“时间”这一概念。

PS:所有的关于游戏循环的问题大多都是关于“时间”的问题,所以“时间”这块一定要好好把握。


固定帧率游戏循环

那如果我们将其定死呢?

无论什么运行速度的电脑,我们一律按照每秒30帧的频率更新游戏内的逻辑并且渲染?

这当然可行,对于好电脑来说(上面例子的电脑A),只要我们让其在每帧更新、渲染结束之后Sleep一会儿就可以了。用图来说是这样的:

对于基础速率大于30FPS的电脑,让其停顿一段时间将帧率固定在60。

用代码实现是这样的:

float MS_PER_FRAME = 33.33f; // 每一帧应该消耗多久,用毫秒为单位,计算方法为 1000/FPS。
bool isRunning = true;
while (isRunning)
{
    double start = getCurrentTime();
    processInput();
    update();
    render();
    sleep(start + MS_PER_FRAME - getCurrentTime());
}

PS:这实际上来说已经可用了,因为如果一台电脑非常慢的话,它也只会按照它能力的最大刷新率来更新游戏(因为你不能sleep一个负值)。但是,虽然能用,这个方法还是不够好。如果一台电脑很厉害,我们应该让其拥有能力去显示更棒的画面、更流畅的动画,而不是单纯地给它定死在一个值上面,于是我们就有了下面的想法。


时间驱动不固定帧率游戏循环

在这个方法中,我们想要的是“正常的游戏时间”(指并不因为电脑太快而让游戏内时间飞快变得没法玩)和“尽可能的更多细节”(用更多的update()和render()来丰富细节)。于是就有了这一种游戏循环:

double lastTime = getCurrentTime();
bool isRunning = true;
while (isRunning)
{
    double current = getCurrentTime();
    double elapsed = current - lastTime;
    processInput();
    update(elapsed); // 因为要随着现实时间去更新游戏,所以在update方法中加入一个“经过时间”的参数,方便其在不影响游戏逻辑时间时更新更多的游戏细节。
    render();
    lastTime = current;
}

这个方法完美地解决了以上两个问题,现在游戏可以在不同的硬件上以同样的速率运行了,而且在好电脑上玩家的体验也更丰富了。但是这个方法有着潜在的问题:不确定性。

这可能就会导致这个问题:在好电脑和坏电脑上我们的输入最后造成的结果不一致了。

PS:举个栗子:如A和B在联机打一款游戏,是用我们刚才说的这个方法做的游戏循环。

A的电脑很棒,能达到每秒50帧的帧率,而B的电脑不太好,帧率只有5。A在游戏中射出了一发子弹,数据传输到B的电脑中,于是这两颗子弹在他们的电脑中被同时发射出去了(在同一帧中)。假设这个子弹会在现实世界中的一秒之后消失,那么从它被射出到消失之前,子弹的位置在A的电脑中被更新了50次,但是在B的电脑中只被更新了5次。由于计算机操作浮点数带来的偏差,这颗子弹最后消失的位置在他们两台电脑中是不一样的!

因为A的电脑累积的误差量是B的电脑的10倍!

所以说,这个方法对于简单的单人游戏来说还可行,但是对于多人游戏来说就不是很可行了。(但是这个方法导致的物理有关的计算的误差仍然有可能让游戏变得不可玩,即使是在单人游戏中)。


灵活帧率更新

我们需要对上述继续进行改进。

我称之为灵活帧率刷新——这个方法的名字确实很难想,因为FPS并不固定,所以就起了个这个名字。以下,请看我对其的解释吧:

PS:由于渲染和更新这两个步骤并没有明确的关联,我们在渲染的时候并不需要考虑两次更新之间经过了多少时间,所以我们可以把其单独拎出来,并不必须要求在每一帧更新之后渲染。

对于这个方法,我们首先钦定一个“最优更新间隔时间”,这是实现游戏中最棒的效果所需要的两帧之间的现实时间间隔。之后对于那些能跑到这个速度的电脑,我们让其按照这个方式跑,对于那些不能的,我们则让其按照自己的节奏去跑。这样做既保证了“好电脑”上的流畅性,也保证了“坏电脑”能玩,还保证了所有电脑上游戏速率是相等的!(而且相比于上一个办法,这个方法去除了让时间直接参与更新的函数,从而让物理等模块更稳定了)。


具体代码实现如下:

float MS_PER_UPDATE = 16.66f; // 60帧时候的每帧间隔。(注意,和之前例子里面的不一样,这个并不是两帧渲染之间的间隔,而是更新的间隔)

double previous = getCurrentTime();
double lag = 0.0f;

bool isRunning = true;
while (isRunning)
{
    double current = getCurrentTime();
    double elapsed = current - previous;
    previous = current;
    lap += elapsed;
    processInput();

    while (lab >= MS_PER_UPDATE)
    {
        update();
        lag -= MS_PER_UPDATE;
    }

    render();
}

用图画出来的话是这样的:

将渲染单独拎出去,在两次渲染之间时间相差过大的时候通过调用多次更新将这个时间差“追回来”。

具体来讲,这个的实现思路是这样的:

在允许一定误差的情况下(每秒+/-1次更新),以固定的速率更新游戏。这样使得各种模块更稳定,至少不会在不同的机器上累积不同的误差而导致同一速率的游戏结果相差甚远。用代码来看的话,我们在每一帧的开始更新lag变量,代表了游戏时钟相对于现实时间所落后的差量,之后使用一个内部循环来更新游戏,让其追赶上现实中的时间(让lag小于一次更新的间隔),之后渲染,进行下一次循环。

PS:这个方法很好,在保证了在好电脑上的游戏效果的同时也保证了“坏电脑”和“好电脑”更新的结果是一样的,虽然他们的帧率可能不一样(渲染的速度)。但是这个方法仍然有弊端,就是这个每帧之间的间隔需要被规范地设置。如果在最不好的机器上,每一次调用update()所需要的时间超过我们规定的时间,它就会陷入死亡循环。每一次循环都需要做比上一次更多的update()来追上时间,结果反而是所需要追的时间更多了。

// 模拟一下,假如我们的MS_PER_UPDATE被设定为16,但是在某一台电脑上它需要20毫秒去完成一次update()。渲染、获取输入需要的时间是10毫秒。
开始(第一轮循环):
current = 0
elapsed = 0
previous = NULL
lag = 0

第二轮循环:
current = 10
elapsed = 10
previous = 0
lag = 10

第三轮循环:
current = 20
elapsed = 10
previous = 10
lag = 20 // 进入内循环,调用一次update()
之后的lag = 4

第四轮循环:
current = 50 // 上一次的一次update 20 + 渲染、获取输入 10 = 30
elapsed = 30
previous = 20
lag = 34 // 进入内循环,调用两次update() 第一次使lag降为18,第二次使lag降为2
之后的lag = 2

第五轮循环:
current = 100 // 上一次的两次update 40 + 渲染、获取输入 10 = 50
elapsed = 50
previous = 50
lag = 50 // 进入内循环,调用3次update()...

第N轮循环...

每次所需要的循环越来越多...导致游戏越来越卡。

所以,这就是为什么要好好规定MS_PER_UPDATE的原因了。我们也可以通过一些其他方法来消除这个问题,比如说增加内循环上限之类的。虽然这样会让游戏变慢,但是也比卡死好,万一真的有那些“一帧不卡两帧电竞”的玩家呢(严肃?


之后的新问题

由于我们的这个方法中存在残留的延迟,我们会在“随机的时间点”进行渲染(渲染和更新并没有直接的关系了),所以在游戏常常会在两帧之间展现出完全相同的画面,或者说一次渲染和上一次渲染差了好几帧。用时间轴来看就是这样的:

蓝色的箭头代表了更新的时间点,红色箭头代表了渲染时间点。(蓝色箭头的间距相等)

这也就出现了这个问题:

一次渲染在两次更新之间

比如说一个子弹在上一次更新在左侧,下一次更新在右侧。这样的话单看这个片段,我们的子弹就会“瞬移”,因为我们并没有在子弹路过面前的时候渲染。但是我们既然已经有了lag这个变量,就代表了我们进入下一帧的时间间隔,所以我们就可以在渲染的时候做点文章了。


假如说我们将渲染改成这样:

render(lag / MS_PER_UPDATE);

PS:我们在其渲染的时候给它这个时间间隔,让其在这种情况下可以“推算”物体在这时候应该在什么位置,并且将其渲染出来,这会提升很多的流畅性。

当然,这个方法有的时候会预测失败,但是由于只有一帧,下一次渲染就会纠正这帧的错误了,所以也还好,相比于没有预测的硬生生的卡顿、瞬移,这个结果还算是可以接受的。


总结

一些设计决策

主要是一些比较细枝末节的东西。


谁来控制游戏循环?

你有几种选择:

  • 平台:比较简单,不用担心游戏核心循环的代码和优化问题,但是失去了对时间的控制。
  • 游戏引擎:无需自己编写,但是当你的游戏的需求和游戏引擎提供的不同的时候,很难办。
  • 自己编写:麻烦,但是写好了之后可以完美的迎合你的需求。

能量损耗

对于手机来说尤甚。通过这章的讲解,有两种循环方式:

  • 能跑多块跑多快:给玩家最棒的游戏体验,但是如果要在移动端这么干可能会把手机变成暖手宝。
  • 限制帧率:对于移动端很友好,可以帮助设备节省电池损耗。

以上就是关于游戏循环的基础内容了,像是现在的游戏引擎的游戏循环是十分复杂的,比如说这是Unity的游戏循环:

Unity - Manual: Order of execution for event functionshttps://docs.unity3d.com/Manual/ExecutionOrder.html链接放在上面了,看上去很是复杂,这就和我们刚才说的不是一个量级的了。但是基础性是归一的,你看那些跑来跑去的箭头,是不是感到一丝丝的熟悉哈哈哈哈。

总之,希望这些知识可以帮到你!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/42284.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

网络基础—网关、网段、子网掩码

接上个笔记,工作时发现看似在一个网段的2个IP地址却ping不通,这里就是子网掩码发挥了作用。 IP地址 首先从最熟悉的IP地址开始介绍,IP地址是唯一标识,由32个0和1构成,长度为32bit,如00001010000000000000…

序列模型之循环神经网络(二)

目录 一.语言模型和序列生成 二.新序列采样 三.带有神经网络的梯度消失 四.GRU单元 一.语言模型和序列生成 上图的例子就是咱们的手机上的语音转文字输入差不多,说一句话,可能有多种可能的句子,那么到底是哪一种呢,咱们的语言…

【EDA365电子论坛】硬件人经历南下、北上,回乡创业,后悔了吗?

南下?北上?留下来?亦或是回乡?这些问题或许是每个电子人都曾遇到过的,如果可以,谁不想留在自己所熟悉的地方呢,但生活,总会让你想要的更多,只能选择再前行一步。 前几天在…

LeetCode题目笔记——2486. 追加字符以获得子序列

文章目录题目描述题目难度——中等方法一:一次遍历代码/C总结这个是上周末的周赛题目,当时忘做了,晚上的时候才想起,可惜了,题目还挺有意思的,类似的好像在以前做过,题目联机在这题目链接 题目…

你好,法语!A2知识点总结(4)

4.固定表达 4.1 devoir faire 4.2 ne…que ne…que:只…,仅仅 注意 用ne…que替换句子 Ex : En ce moment, il mange surtout des crales. En ce moment, il ne mange que des crales. 1)Elle lit surtout des romans. Il fait surtout du tennis.Ils s’intres…

NeRF算法Keras实现教程

在这个教程中,我们展示了 Ben Mildenhall 等人的研究论文 NeRF:将场景表示为用于视图合成的神经辐射场的最小实现。作者提出了一种巧妙的方法,通过神经网络对体积场景函数进行建模来合成场景的新颖视图。 为了帮助你直观地理解这一点&#x…

Mac下adb性能测试实战

本次实验以Editor App为例 1.测试Editor启动时长 打开终端,依次输入如下命令: adb devices 查看设备是否连接 adb shell 进入手机系统 adb devices pm list package -f | grep vidma 获取Editor包名 dumpsys package vidma.video.editor.video…

[glacierctf 2022] 只会3个

目录 Crypto CryptoShop 完成 Strange Letters Simple Crypto ChaCha60 Unpredictable Misc The Climber Size Matters ClipRipStage1 pwn Break the Calculator old dayz File-er rev Whats up Sandboxer 这个比赛完事后马上关网站。想复现都困难。会的不…

【宝塔面板安装与配置、Redis安装与配置、MySQL安装与配置】

提示:宝塔面板下载地址:https://www.bt.cn/new/download.html 文章目录前言一、快速迁移二、设置固定ip一.保证可以连接网络二.设置固定ip三、搭建宝塔面板四、做好备份五、安装Redis六、安装MySQL一、8.0版本以下二、8.0版本以上三、安全组开放端口四、…

GitHub上最全的Java面试题库竟还要收费?黑客强行开源后遭起诉

“金三银四”、“金九银十” 一直被称为是跳槽的旺季,最近也有不少小伙伴找我咨询一些面试相关的问题。但是我的建议是跳槽之前先想清楚自己为什么要跳槽,不要看到身边的同事跳槽之后薪资涨了不少,没有做好充分的准备就盲目开始投简历去面试。…

最新版 Cesium(1.99.0) 构建封装开发环境以及遇到问题

最新版 Cesium(1.99.0) 构建封装开发环境以及遇到问题使用版本Webpack4.* 集成 Cesium 错误官方示例介绍官方示例配置介绍:官方示例配置补充:webpack5 过程学习资料githup 完整项目最近项目用 Cesium 比较多,因此想把常…

世界各国GDP相关面板数据(1960-2019年)

1、数据来源:世界银行 2、时间跨度:1960-2019年 3、区域范围:全球 4、指标说明: 该数据包含以下指标: 1.世界各国1960-2019年GDP 2.世界各国1960-2019年GDP增长率 3.世界各国1960-2019年人均GDP 4.世界各国196…

SwiftUI 精品源码之学习可视化应用程序基于SceneKit,AVSpeechSynthetizer 文字转语音功能tts(教程)

我是一个非常注重视觉的人,这也适用于我的学习。当他们做某些事情时,我通过观察事物或其他人来学习。我也有很强的听觉倾向,所以你不能指望我读完一本 300 页的小说,但你可以指望我听 1 小时的播客而不会分心,并从中真正学到一些东西。 这种学习方式时常帮助我,从一门学科…

龙蜥开发者说:开源是场马拉松!来自广州大学姚同学的开源成长记 | 第 13 期

「龙蜥开发者说」第 13 期来了!开发者与开源社区相辅相成,相互成就,这些个人在龙蜥社区的使用心得、实践总结和技术成长经历都是宝贵的,我们希望在这里让更多人看见技术的力量。本期故事,我们邀请了龙蜥社区开发者姚胤…

远程兴起,前端音视频通话?学

前言 看到前两篇文章都非常受欢迎,加起来有一千多赞了👍🏻 ,非常的开心!🤖继续加油!冲! 我们在前两篇文章中大概讲解了 音视频媒体流的获取,处理,以及在上一…

(免费分享)基于jsp的CRM客户管理-带论文

一、课题来源: 自有商业活动,客户关系就一直是商业活动中的一个核心问题,同时也成了商务活动成功的审核标准。在全球经济一体化为时代背景的时代,企业面临着为公司带来更多的客户、为管理者提供准确的信息和便捷的操作查询手段 、…

新旧iphone短信转移,苹果旧手机短信导入新手机

短信携带重要信息内容,新旧iphone短信转移?您可能知道,iOS设备上不支持导出iPhone简讯,更不用说打印iPhone上的短信了。幸运的是,有一些可行的方法可以将iPhone短信导入到另一个iPhone,继续阅读以获得更多帮…

软件协会第01次活动第05次任务布置:爱心代码+演奏歌曲+typora使用pandoc导出+github注册登录+函数练习+写csdn文章

了解更多关注软协官网:https://www.csuftsap.cn/ 来自软件协会编辑,注册会员即可获取全部开源.md资源,请勿转载,归软件协会所有。 文章目录:star: 软件协会第01次活动第05次任务布置1.程序员的浪漫-爱心代码(升级版&a…

css3手册

文章目录布局弹性盒生成弹性容器和弹性项目更改方向主轴排列侧轴排列弹性项目伸缩主轴换行网格生成网格布局定义行和列改变排列方向单元格之间的间隙单元格内部的对齐网格项目定位视觉阴影盒子阴影文字阴影圆角背景渐变变形translate 平移scale 缩放rotate 旋转改变变形原点多种…

spring MVC源码探索之AbstractHandlerMethodMapping

AbstractHandlerMethodMapping 是什么 官方解释是这样的。 /*** Abstract base class for {link HandlerMapping} implementations that define* a mapping between a request and a {link HandlerMethod}.** <p>For each registered handler method, a unique mapping…