游戏循环是一种典型的游戏编程范式,在游戏之外的领域很少用到。
首先来看客户端的游戏循环,伪代码如下:
while (true) {
processInput() // 处理(用户、网络)输入
update() // 更新游戏状态
render() // 渲染
}
有两个相关的术语:
具体实现有以下四种方式:
服务器游戏循环和客户端类似,但是没有渲染环节:
while (true) {
processInput() // 处理(网络)输入
update() // 更新游戏状态,包括定时器
}
最简单的固定帧率实现:
int MS_PER_TICK = 100 //每帧100ms
while (true) {
long start = now()
processInput()
update()
long elapsed = now() - start //流逝的时间
if (elapsed < MS_PER_TICK) {
sleep(MS_PER_TICK - elapsed)
}
}
但是有一个问题,如果一帧消耗的时间大于100ms,会导致计时不精确。
有两种解决方式,第一种是追帧:
long nextTickTime = now()
while (true) {
processInput()
update()
nextTickTime += MS_PER_TICK
long now = now()
if (nextTickTime > now) {
sleep(nextTickTime - now)
}
}
另一种是不固定帧率,update()传入两帧间隔时间:
long lastUpdate = now()
while (true) {
long start = now()
processInput()
update(start - lastUpdate)
lastUpdate = start
long elapsed = now() - start
if (elapsed < MS_PER_TICK) {
sleep(MS_PER_TICK - elapsed)
}
}
这样就能保证精确计时了。和追帧的方式相比,不固定帧率可以减缓服务器负载。
还有另一个问题,上述实现每帧调用一次 processInput(),导致处理用户请求有最多100ms的延迟。改为实时处理用户请求:
long lastUpdate = now()
while (true) {
long start = now()
update(start - lastUpdate)
lastUpdate = start
processInput() //处理已经接收的用户请求
long elapsed = now() - start
while (elapsed < MS_PER_TICK) {
Object input = waitInput(MS_PER_TICK - elapsed) //等待用户请求
processInput(input);
elapsed = now() - start
}
}
其中 waitInput() 为等待用户请求,阻塞调用,参数为最大阻塞时间
实现事件发布者和订阅者的解耦。下图 EventBus 是一个事件中心:
register() 注册监听事件,参数是事件类型和事件处理函数。
unregister() 解除注册。
post() 发布事件。
handlerMap 记录了事件对应的处理函数列表。处理函数可以有优先级。
以《炉石传说》为例,游戏中有以下卡牌:
扭曲巨龙泽拉库:每当你的英雄受到伤害,召唤一条6/6的虚空幼龙。
以眼还眼:当你的英雄受到伤害时,对敌方英雄造成等量伤害。
后院保镖:每当一个友方随从死亡,便获得+1 攻击力。
诅咒教派领袖:在一个友方随从死亡后,抽一张牌。
毒镖陷阱:在对方使用英雄技能后,随机对一个敌人造成 5点伤害。
用事件驱动的方式,可以将“英雄受到伤害”、“随从死亡”、“使用英雄技能”等定义成事件,卡牌上场时注册对应事件,离场时解除注册。这样,当新加一种卡牌时,不需要修改原有代码,只需要在新卡牌的代码中注册事件,最终实现了事件发布者和订阅者的解耦。
所谓同步,就是要多个客户端表现效果是一致的。实现同步有帧同步和状态同步两种方式。
(以魔兽世界为例)
帧同步 | 状态同步 | |
---|---|---|
开发难度 | 低 | 高 |
安全性 | 低 | 高 |
一致性 | 高 | 低 |
响应性 | 低 | 高 |
服务器压力 | 低 | 高 |
断线重连 | 困难 | 容易 |
回放 | 容易 | 困难 |
跨平台 | 困难 | 容易 |
ELO等级分是指由物理学家Elo创建的一个衡量各类对弈活动水平的评价方法,是当今对弈水平评估的公认的权威方法。被广泛用于国际象棋、足球、游戏天梯排名等。
假设A和B的等级分为RA和RB,则按Logistic distribution, A对B的胜率为:
假如一位棋手在比赛中的真实得分SA(胜=1分,和=0.5分,负=0分)和他的胜率期望值不同,则他的等级分要作相应的调整。具体的数学公式为:
K值常规比赛通常为32,大师级比赛通常为16。
例如,A等级分为1613,与等级分为1573的B战平。若K取32,则A的胜率期望值为
因而A的新等级分为
一种时间复杂度为 O(1) 的随机采样算法。
Alias Method离散分布随机取样
AOI(Area Of Interest),即感兴趣区域,指玩家在场景中的视野区域。
AOI的统一接口如下:
有了AOI之后,场景同步可以只同步AOI内的角色行为。AI感知也可以基于AOI实现。
注意:帧同步必须使用全场景同步。
将场景划分为一个个的小格子,每个格子记录处于其中的角色,我周围的格子里的角色,即为AOI兴趣角色。
网格法不适用于人数很多、场景很大的情况。
网格法图解
KBEngine 中的十字链表实现
MMO游戏的场景中有许多角色,会频繁产生大量的属性变化,如果每次属性变化都进行同步则开销太大。而使用脏标记模式,在属性变化时将属性的标志位标脏,周期性的将标脏的属性同步给所有客户端,这样可以很大程度上减少同步量。
同样,将变化的属性记录到Redis缓存或数据库中,也可以使用脏标记模式。
脏标记可以用位来记录,这样一个long可以记录64个脏标记。也可以用set记录。
AI在游戏中有大量应用,比如人机模式的战斗AI、怪物AI、玩家挂机AI等。
AI的实现一般有状态机和行为树两种方式。
状态机是一种表示状态并控制状态切换的设计模式。角色处于某个状态中,状态包含数据以及事件处理逻辑,当一个事件发生时,会触发一个动作,或者执行一次状态的迁移。
但是状态机有一些缺陷:
而行为树可以很好地解决这几个问题。
行为树是一棵用于控制 AI 决策行为的、包含了层级节点的树结构。树的最末端——叶子,就是这些 AI 实际上去做事情的命令;连接树叶的树枝,就是各种类型的节点,这些节点决定了 AI 如何从树的顶端根据不同的情况,来沿着不同的路径来到最终的叶子这一过程。
Unity 和 Unreal 引擎都内置了可视化的行为树框架。
AI 行为树的工作原理
Unity 行为树
深度学习也可以用于游戏AI,但目前使用不广泛,用到的有《王者荣耀》、《逆水寒》
、《星际争霸2》等,而且也是小规模应用。原因有:
网易游戏——伏羲人工智能实验室
可以理解为Trie树+KMP算法。
一个常见的应用场景就是:给出n个单词,再给出一段包含m个字符的文章,找出有多少个单词在文章里出现过。
在游戏中常用于屏蔽词过滤。
AC自动机算法图解
和UDP相比,TCP协议有以下特性:
但是,TCP也存在一些问题: