转:游戏中的网络同步机制——Lockstep

0x00 前言

每个人或多或少都接触过网游,那个虚拟的世界给予了我们无穷的乐趣,而这个虚拟世界是如何完美的将身处天南地北的玩家连接在一起的呢?我们每个人的电脑配置都不一样,网络延迟也不同,但是在玩FPS(第一人称射击)游戏时,战斗感受与真实世界并无二致,网游是如何做到这一点的呢?

本文将介绍和分析早期广泛在RTS(即时策略)游戏中应用的同步机制——Lockstep

RTS游戏有很多,比如我们都玩过的的Warcraft III(大家耳熟能详的Dota是它的一张地图)和StarCraft,还有EA的代表作命令与征服系列(Command & Conquer)等等,以及现在非常流行的Dota2LOL

那么为什么要强调早期呢?因为Dota2LOL等新兴的游戏使用的同步机制不再是传统的Lockstep了。严格来说,Warcraft和现在意义上的网游有很大区别,因为它所谓的网是局域网(LAN)。早期RTS游戏出现时互联网还没有现在那么普及,网速也很慢,更没有什么像样的网游,能够支持局域网对战已经很不错了。

有人可能会有疑问,我们平时经常在对战平台上和全国各地的人打Dota,你为什么说Warcraft III只支持局域网呢?这又是一个很有意思的话题,实际上,对战平台使用了虚拟局域网(VLAN)技术,通过进程注入,HOOK WinSock函数调用,将数据包发送到对战平台服务器上,由服务器分配虚拟IP,这里还能够进行天梯匹配等等,在随后的游戏过程中游戏数据包都是通过对战平台的服务器进行转发,但是这一切对Warcraft III进程本身来说是透明的,它依然感觉自己在一个局域网环境中。

0x01 为什么要有同步机制

一致性

在虚拟世界中,保证游戏的一致性是一个基本前提。什么是一致性?通俗的说就是虚拟世界中的事实,比如在一个FPS游戏中,大家的延迟都很高,A、B两个玩家同时发现了对方,并向对方射击,如果没有很好的同步机制,那么A的屏幕上显示B还没有开枪就被击杀,而B的屏幕上显示A还没有开枪就被击杀,这就出现了不一致的问题,那么这个游戏还怎么愉快的进行下去?

可以这么说,延迟是造成不一致问题的主要原因。如果延迟都为0(即A玩家作出行动的同时B玩家就能看到),那么也就不存在不一致的问题了,就像在真实世界中一样。而同步机制除了基本的通信作用外,最重要的任务就是解决不一致问题,即保证游戏的一致性。同步机制有许多种,根据游戏类型、技术条件甚至时代背景的不同,选择的同步机制也会不同。

分类

游戏的网络同步机制有很多,国外也有这方面的论文,抛开具体实现细节,总体来看可以分为下面几类

  • Peer-to-Peer,在这类方法中,没有服务器,游戏参与者的身份是对等的,依靠参与游戏的玩家电脑自行解决同步问题的,最为典型的就是Lockstep
  • Client-Server,在这类方法中,Server端是绝对的权威,所有计算基本在Server上完成。例如,在游戏中向前移动一步,要等待服务器确认“你向前移动了一步”之后,才可以在客户端上进行这个行为。(延迟较低的时候是察觉不到这个过程的,延迟高时会有明显的卡顿现象)
  • Client-Side Prediction,严格来说这并不是一类方法,而是对第二类方法的改进。试想如果所有的操作都必须在得到服务器的确认,然后才在客户端上进行,在延迟较高时用户体验会非常的差。这时可以把常用一部分计算转移到客户端进行,服务器辅助校正即可。

0x02 什么是Lockstep

Lockstep最初是军队行进中使用的,后来在19世纪的时候广泛在美国监狱使用,成为那个时期美国监狱的一个标识。就像这样

WAeN1vjpp6pDTWCO1J5QPhGlHRiJ60vKXuXxAmK1

 

或者这样

rRiG7oC1Lma1suliNdBAU737nFDJnWt9LBHNCkF9

 

意思就是大家同步的走,谁超前了要等待,落后了的要赶上。后来就引申到游戏的网络同步机制上了

上一章节中我们说到Lockstep是Peer-to-Peer架构中的一种同步方式,而我们平时在局域网中玩Dota时,也的确没有大型的游戏服务器,只有一台所谓的主机,那么你可能会想,是不是所有的计算都是在那台主机上完成的呢?也就是说其他玩家的机器只发送施放了某某技能这样的数据包给主机,而造成多少伤害、某某效果是由主机计算并返回的。

但事实并不是这样的,玩Dota所有的一切都是在本地计算完成的。包括技能伤害、效果,命中与否,随机刷新野怪等等。也就是说每个玩家的电脑都完整计算了整盘游戏的全过程,且计算过程与计算结果都一模一样。而主机只是负责把每个玩家的操作指令(鼠标点击、键盘按键等等)广播给其他玩家。

是不是感到难以理解?

想要理解Lockstep的机制,先看看下面三个问题。

  • 什么是动画?是会动的画吗?当然不是,人眼的记忆时间为0.1s,也就是大约100ms,只要把一帧一帧的静态图像快速播放一遍,我们就会感觉画面就动了起来,比如下面这样PY28PKq2E3P24OWxeKV2GIRjxfoKCE5yeSf74bnY

 

  • 最容易实现同步的游戏类型是什么?当然是回合制游戏,比如棋类游戏和卡牌游戏,它们有严格的先后顺序,不容易出现逻辑错误,更不会出现不一致的情况;而且回合时间较长,能够容忍高延迟。
  • 什么是状态机?状态机是表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型。给定一个状态机模型F,处在某一个状态S1,这时给定一个输入I,此时状态机会转移到一个新的状态S2。在这个过程中,只要FS1I是确定的,那么S2就是确定的。

其实把以上三点结合起来,就是Lockstep的基本思路

我们来看下面这张图

VWRJNtLhYdWxr9bxLzDPfjwdSOl4CxFG12eJ42Fo

图中是A、B、C三个玩家的时间轴,这个时间轴不是电脑上的本地时间,而是A、B、C联机时定义的一个时间轴。虚线分隔出来时间片称为turn,可以理解成一个回合。箭头表示该玩家将自己的操作指令广播给其他玩家。我们把一盘游戏看成一个大型的状态机,因为大家玩的是同一款的游戏,因此F是相同的,初始状态S0也是相同的。在第一个turn结束时,所有玩家都接收到了完全一样的输入I,注意这里的I不是一个值,而是包含了当前游戏中所有玩家的操作指令集合。t1时刻所有玩家的电脑自行计算结果。由于FS0I是固定的,所以每个玩家电脑上计算出的下一个状态S1一定是相同的。

同理,第二个turn也是如此qzobYsq3v0JyMYKr9ma6mqjiVNWIddttuINF0cbx

 

可以看出,Lockstep其实也是“回合制”的,当然这个所谓的回合与我们理解的棋类、卡牌游戏的回合是不太一样的。Lockstep的回合(也就是turn)中,所有玩家都可以采取行动,最终结果是在回合结束时统一计算的。在同一个turn接收到的操作指令,是不分行动先后顺序的,只要是在同一个turn里,就认为是同时发生的。

举个例子,假设A、B、C是游戏中3个互相敌对的单位,攻击力都为100。在某一个turn内,A和B都右键点击了C(warcraft这类游戏好像都是右键普攻),C右键点击了A,这些操作指令都广播到了其他玩家电脑上,则该turn的输入为“A攻击C、B攻击C、C攻击A”。那么该turn结束后,每个人的电脑都开始计算,且计算结果是相同的,即“A损失100生命值,B不变,C损失200生命值”。

这就是Lockstep同步机制,其实也没有多复杂是吧~这里还有几点需要注意:

  • Lockstep把游戏过程划分成了一个个turn,为什么游戏不会出现卡顿的现象呢?回到前面动画的那个问题,人眼是容易被欺骗的,人的反应其实也是很慢的(相对电脑来说)。人眼的记忆时间为0.1s,只要每秒进行10turn,我们是感觉不出卡顿的。而通常游戏的帧数为60fps,即每秒60幅图像在屏幕上显示,你会感觉游戏非常流畅~当然“帧”和Lockstep中的“turn”并不是一一对应的,这里只是想说明一个turn的时间是非常短的,至少比我们的反应要快的多。
  • Lockstep对网络延迟的要求是非常高的,因为每个turn要向所有玩家广播操作,同时也要接收来自其他玩家的操作。只有当每个turn集齐了所有玩家的操作指令,也就是输入确定了之后,才可以进行计算,进入下一个turn,否则就要等待最慢的玩家。当然局域网可以很好的满足这个要求,延迟基本都在1ms左右。
  • Lockstep中会不会出现延迟导致的不一致问题?显然不会,从上面的分析可以知道,使用Lockstep的游戏是严格按照turn向前推进的,如果有人延迟比较高,其他玩家必须等待该玩家跟上之后再继续计算,不存在某个玩家领先或落后其他玩家若干个turn的情况。使用Lockstep同步机制的游戏中,每个玩家的延迟都等于延迟最高的那个人。(当然这个说法还有待讨论,后面还会说到这个问题)
  • Lockstep是非常严格的,要求每一步的的计算结果都完全一样,任何计算错误都有可能导致蝴蝶效应,产生严重的后果,因为状态不能同步的话游戏根本就没有办法进行下去,最终将崩溃退出。
  • 《Algorithms and Networking for Computer Games》这本书中关于Lockstep的部分与本文的说法有些不同,我觉得也没有谁对谁错之分,Lockstep更多的是一种思想,算是不一样的理解吧,感兴趣的同学也可以看看书中的章节。书中将Lockstep分为commit和reveal两个阶段,并且采用了流水线的方式。如下图所示

sBEhIr9XvJJtC7vSfguIHHiILopdq0veg3lrLmbUgpok2fQ9LXAx8MAPhErQKE8kbJDndV0FNK0OktDJ

 

0x03 与Lockstep无关的细节问题

为什么要讨论与Lockstep无关的问题呢?因为这些问题虽然与Lockstep无关,但却与游戏本身有关,而且很多问题是由Lockstep引起的

野怪刷新与暴击

我们都知道,Dota中有许多问题是与概率相关的,比如整点时野怪是随机刷新的,出了水晶剑之后是有概率暴击的。那么按照Lockstep同步机制,计算都是在每个玩家自己电脑上完成的,那么在有概率存在的情况下,怎么可能保证每台电脑的计算结果一致呢?!

这时就轮到伪随机数派上用场了,我们来看下面这个Java程序

import java.util.Random;
public class PseudoRandom {
    public static void main(String[] args) {
        Random r1 = new Random(10);//这里的10就是随机种子(Random Seed)
        for (int i = 0; i < 10; i++) {
            System.out.print(r1.nextInt(100) + "t");
        }
        System.out.println();
        Random r2 = new Random(10);
        for (int i = 0; i < 10; i++) {
            System.out.print(r2.nextInt(100) + "t");
        }
    }
}

//Output
//13	80	93	90	46	56	97	88	81	14
//13	80	93	90	46	56	97	88	81	14

 

如果你的jdk版本没有什么问题的话,那么你看到的结果一定也是上面那样,而且无论你运行多少次都是这个结果。大部分编程语言内置库里的随机数都是利用线性同余发生器产生的,如果不指定随机种子(Random Seed),默认以当前系统时间戳作为随机种子。一旦指定了随机种子,那么产生的随机数序列就是确定的。

所以,游戏开始前,参与游戏的玩家电脑协商确定一个随机种子,就可以保证在游戏进行过程中大家产生的随机数序列是相同的,也就可以保证计算结果一致。

例如一个英雄的暴击率为30%,对某个目标持续普攻,如果随机数序列为12 32 90 25,小于等于30判定暴击,大于30判定不暴击,那么每个玩家电脑的计算结果都是暴击 不暴击 不暴击 暴击。(这里只是举例说明原理,游戏中的实现细节不一定是这样)

外挂问题

在对战平台打Dota的人都见识过全图挂,而且屡禁不止,那么为什么Warcraft III中会有全图挂,而没有其他游戏中的变态挂(如加强攻击力,瞬间移动等等)呢?为什么无法从根本上杜绝全图挂呢?

前面已经说过了,使用Lockstep同步机制,所有计算都是在本地完成的,每轮turn都必须接收来自其他所有玩家的操作指令才能完成计算,也就是说其他玩家的一举一动你的电脑其实是知道的,只不过没有在你的屏幕上展现出来罢了(被战争迷雾遮挡了)。因此,全图挂只要修改Warcraft III的内存数据,就可以去除战争迷雾,达到开全图的效果。而对战平台不可能改变Lockstep这种同步机制,只能在本地检测是否有其他程序修改Warcraft III的内存数据(就像病毒查杀一样),而外挂程序总有办法绕过检测,所以全图挂总是层出不穷。

而另一方面,恰恰因为Lockstep同步机制,其他比较变态的外挂根本不可能在Warcraft III上存在。比如强化攻击力,我们同样可以利用修改内存的方式将增加自己的攻击力,可以直接秒杀其他任何单位,但是别忘了其他玩家电脑也在同步的进行计算,而我们的攻击力在其他玩家电脑上仍然是不变的,这就造成了状态不一致。前面说过了,Lockstep中出现状态不同步的情况时很容易产生蝴蝶效应,最终崩溃退出。

断线重连

这个问题一直被广大玩家诟病,因为其他网游从来没有断线之后连不回去的情况,为什么Warcraft III不支持断线重连呢?

Lockstep同步机制是非常严格的,中途加入游戏是从技术上来讲是非常困难的。首先中途加入的玩家要进行状态同步,而状态同步本身包含的内容太多了,其中包括时间戳、伪随机数序列、所有单位的位置属性信息等等,不是简单的复制粘贴就能同步的。这说起来容易,要具体实现起来没那么简单,而且在局域网中掉线情况并不常见,因此早期暴雪的开发人员可能也就忽略这个问题了。

事实上使用对战平台的人才会经常遇到这个问题,在互联网中,延迟高、掉线的情况时有发生。那么11平台的掉线重连功能是怎么来的呢?个人观点,那并不是真的掉线重连,只是我们的丢包情况比较严重,暂时卡了而已。11平台可能做了一些优化,将其他玩家的操作指令保存起来,等延迟稳定的时候再发送给我们。但由于此时我们电脑所处的turn可能落后了其他玩家,所以此时游戏就会像快放一样赶上其他玩家的进度。如果是真的掉线,如停电、程序崩溃等直接退出的情况,我们是无法重新加入游戏的。

Warcraft III是不是使用严格的Lockstep机制?

接上一个问题,如果Warcraft III是严格的Lockstep同步机制,那么一定会出现一人卡、大家都卡的情况,而事实上只有当我们是主机时才会出现这种情况,其他情况下我们延迟高甚至掉线都不影响其他玩家的操作。

因此,Lockstep实际是一种理想的模型,如果在实际中使用会造成非常差的用户体验。那么Warcraft III使用的到底是什么同步机制呢?参考What every programmer needs to know about game networking这篇文章后面一个评论的说法

TOADCOP

FEBRUARY 10, 2010 AT 9:15 AM

btw afaik StarCraft don’t use peer-to-peer it uses client-server model with lockstep (at least warcraft 3 does so). It has the advantage what theoreticaly laggers will not affect gameplay/response latency at all (but to not let them fall behind server do timeouts so the lagger can catch up, also doing temporary local game speed increasing) and imo it’s the only and true way to do sync in RTS like games. (and for some reasons this technic isn’t good covered in the web)

然后是作者的回复

GLENN FIEDLER

FEBRUARY 10, 2010 AT 10:20 AM

You are correct. Also something cool is that in a C/S RTS model the server could also theoretically arbitrate to ignore turns from lagging players, and kick them if they don’t catch up – removing various exploits where you can time-shift your packets and lag out other players.

也就是说Warcraft III使用的是基于Client-Server的Lockstep模型。这就是为什么Warcraft III中有主机这个概念,当然这里主机的作用并不是完成所有计算。

(以下为个人观点)

Warcraft III中的主机的主要功能是广播并设置timeout,也就是说在每个turn内,游戏玩家并非直接将自己的操作指令广播给其他玩家,而是先发送给主机,由主机负责广播,且每个turn都有timeout,如果超过了timeout仍然没有收到某个掉线玩家的操作指令,则忽略该玩家在该turn的行为,即认定他什么都没有做,并与其他延迟正常的玩家同步进入下一个turn。而当掉线玩家网络恢复时,主机会将之前保存的turn中操作指令集合发送给该名玩家,而该名玩家为了赶上进度,就会出现游戏快放的情况。

所以Warcraft III中只有在主机延迟高或掉线时,其他玩家才会受影响,否则不受影响。在局域网中,如果主机是正常退出的,那么会选定另一玩家电脑作为主机,如果是崩溃退出的,则所有人都会直接掉线。至于在对战平台上是否有优化就不太清楚了。

0x04 总结

Lockstep是出现较早的一种同步机制,不过现在很多RTS游戏中依然能够看到它的影子,当然都对它进行了一定程度的改进。

国内关于游戏编程和网络同步的教材、文献寥寥无几,不知道是文化因素还是什么其他原因,难道与游戏相关的技术都是玩物丧志、不学无术?

在我查询资料的过程中,发现国外不仅有游戏编程和网络同步的理论教材,还有大学开设的游戏课程,甚至还有硕士论文是关于设计一个MMORPG游戏的……看看国外繁荣的游戏(使命召唤、刺客信条、魔兽世界),再看看国内繁荣的游戏市场(页游?手游?),看看国外知名的游戏公司(Blizzard、EA、UBISOFT),再看看国内知名的游戏公司(……企鹅?)

这里不是想黑什么,而是觉得计算机作为一门多样化的学科,我们不能固步自封,排斥其中的一些东西。有人说研究游戏有什么用?真的没用吗?看看下面这些应用

  • 网络游戏中广泛应用的航位推测法(Dead Reckoning)美国军方也在使用
  • 游戏中的自动寻路算法,在机器人和自动驾驶中也有应用
  • 游戏中的决策与博弈论也有关联
  • 游戏服务器集群之间的负载均衡与一致性,和现在热门的大数据息息相关

虽然不一定是由游戏本身推动的,但是这些研究之间是相辅相成的,不会做无用功的,何况游戏本身就是增加GDP的一个好手段╮( ̄▽ ̄)╭

本文计划写成一个系列,后续还会介绍其他同步机制~

0x05 相关资料

写在后面

出于转载的目的:原文地址:http://bindog.github.io/blog/2015/03/10/synchronization-in-multiplayer-networked-game-lockstep/

我找到目前的关于网络位置同步文章,较好的文章,所以全程转载记录

Cocos2d-x在XP上完美运行

本文描述cocos2d-x 3.x版本的windows xp版本,即完美在xp系统上运行。

必备:

vs2013 update5 vs2015 update3,笔者系统win10 x64系统
cocos2d-x 3.X版本,已成功运行起来的的版本有3.4 3.5 3.10 3.13版本,其他版没试验,都很类似的修改。

libcocos2d项目属性修改

编译发布版的Release

添加预编译宏

项目菜单->项目属性->配置属性->C/C++->预编译
WIN32_WINNT=0x0501

添加链接库

项目菜单->项目属性->配置属性->链接->输入
user32.lib
gdi32.lib
advapi32.lib
Shell32.lib
ws2_32.lib

cocos2d-x源代码修改:

5>e:sdkcocos2d-x-cocos2d-x-3.10externalwebsocketsincludewin32win32helpers/websock-w32.h(47): error C2065: 'LPWSAPOLLFD' : undeclared identifier (..networkWebSocket.cpp)
5>e:sdkcocos2d-x-cocos2d-x-3.10externalwebsocketsincludewin32win32helpers/websock-w32.h(47): error C2146: syntax error : missing ')' before identifier 'fdarray' (..networkWebSocket.cpp)
5>e:sdkcocos2d-x-cocos2d-x-3.10externalwebsocketsincludewin32win32helpers/websock-w32.h(47): error C2165: 'left-side modifier' : cannot modify pointers to data (..networkWebSocket.cpp)
5>e:sdkcocos2d-x-cocos2d-x-3.10externalwebsocketsincludewin32win32helpers/websock-w32.h(47): error C2513: 'INT *' : no variable declared before '=' (..networkWebSocket.cpp)
5>e:sdkcocos2d-x-cocos2d-x-3.10externalwebsocketsincludewin32win32helpers/websock-w32.h(47): error C2059: syntax error : ')' (..networkWebSocket.cpp)
5>e:sdkcocos2d-x-cocos2d-x-3.10externalwebsocketsincludewin32win32helpers/websock-w32.h(50): error C2065: 'LPWSAPOLLFD' : undeclared identifier (..networkWebSocket.cpp)
5>e:sdkcocos2d-x-cocos2d-x-3.10externalwebsocketsincludewin32win32helpers/websock-w32.h(50): error C2146: syntax error : missing ')' before identifier 'fdarray' (..networkWebSocket.cpp)
5>e:sdkcocos2d-x-cocos2d-x-3.10externalwebsocketsincludewin32win32helpers/websock-w32.h(50): warning C4229: anachronism used : modifiers on data are ignored (..networkWebSocket.cpp)
5>e:sdkcocos2d-x-cocos2d-x-3.10externalwebsocketsincludewin32win32helpers/websock-w32.h(50): error C2059: syntax error : ')' (..networkWebSocket.cpp)

直接注释掉

5>..baseCCConsole.cpp(412): error C3861: 'inet_pton': identifier not found
5>..baseCCConsole.cpp(417): error C3861: 'inet_pton': identifier not found
5>..baseCCConsole.cpp(443): error C3861: 'inet_ntop': identifier not found
5>..baseCCConsole.cpp(450): error C3861: 'inet_ntop': identifier not found

同样注释掉,如果你要用到就自己重写net_ntop函数,网上有教程。

依赖库修改

3.4 3.5版这一步直接跳过
xp错误的提示:InitializeCriticalsectionEx,
curl新版库用了函数,xp系统是没的,删除curl文件夹,再放旧版的,这里下载:http://pan.baidu.com/s/1mhvjlHm
cocos2d-x 3.13删除替换 webp :http://pan.baidu.com/s/1qYLA1ti

编译说明&vs运行库安装

Release每次都会重新编译
1、C/C++  -> Genera -> Debug Information Format: Program Database for Edit & Continue (/ZI), 或者 Program Database (/Zi) ,或者删除None
2、在你工程加入链接库libcocos2d.lib,移除掉libcocos2d的依赖项目
除了f#不用装,都勾上下载地址:http://pan.baidu.com/s/1c1e4ALq ,注意如果是2015安装:https://www.microsoft.com/en-hk/download/details.aspx?id=48145

总结

同样如果你想编译xp项目,也可以做参考,特别是要指定WIN32_WINNT=0x0501。
项目菜单->项目属性->配置属性->常规->平台工具集,选择“VS2013WindowsXP(v120_xp)”。
项目菜单->项目属性->配置属性->链接->系统->5.01
提示不是有效的应用程序,一般是缺少运行库。
提示无法定位程序输入点 xxx 于动态链接库XX上的,就要检查你源代码或者依赖的库里面有XP不存在函数,把函数移除,或重写。

cocos2d-3.X-xp

次世代图形接口Vulkan

Khronos在2015GDC上公布低消耗,跨平台3D 的API(如3D电子游戏)规范命名Vulkan,,作为OpenGL的下一代,提供更高性能和更低的CPU占用率,更好的利用多核,部分组件是AMD公司的Mantle。它的类似者但不能跨平台是微软DX12 苹果公司的Metal。androd 和IOS目前的是GL ES。

例子demo教程:

下一代图像接口-Vulkan教程

发布时间

1.0版本将在2016年初。

概述

直接GPU操作控制

比如直接把数据写入GPU,程序员自己控制内存分配策略

友好多线程架构

同一时间多个线程创建和填冲命令,然后有单独线程提交到GPU

跨平台

统一的API在所有的平台上

着色

接受SPIR-V格式,将提供支持GLSL。第三第三方可以用其他语言编自己的着色器

厂商支持

nvidia,amd,使用安卓系统的厂家,具体的请看驱动程序是否支持,当然得规范正式发布,厂家再根据规范更新显卡驱动。

总结

相当于把驱动的部分控制放给开发应用程序员控制,意味画三角形,更多的代码,带来的好处是不需要和厂家交涉再去更新显卡驱动。

图说明

vulkan
gles-vulkan

左边是vulkan,右边是gl es ,明显看出多核能利用和fps高。如果想了解更多的请看这个:http://vulkan-tutorial.com/

cocos2d-x背景无限滚动

最近项目做一个需要背景无限滚动,适用飞机,跑酷类型无限背景滚动等游戏,在网上找了一些例子,发现都犯严重的错误,就是滚动不和时间挂钩,会看到卡顿现象。每一帧+速度,正确来是s =v*dt,v速度,dt每一帧时间。

原理

现有一张图片,并排到一起是无缝衔接,假设你的分辨率是1280*720,用的图片长度是1552,如放一张图片,是足够长度的,至于为啥要是为了并排的图,能在移动时候,出现短暂黑的。

  1. 两个精灵分别A B 都是用同一张图片
  2. AB 同时移动 speed*dt,这里注意一定要乘时间
  3. 当B的位置移动到<0.0f时(表示后面的图片完全在屏幕上,A已移动到屏幕外面),把A放到B后面,这个时候就是BA。然后又到A移到<0.0f。

滚动代码

资源和图:http://pan.baidu.com/s/1mf2WY

BgScroll.h

#ifndef BGSCROLL_H
#define BGSCROLL_H
#include "cocos2d.h"
using namespace cocos2d;
class BgScroll :public Node
{
public:
	BgScroll();
	virtual ~BgScroll();
	CREATE_FUNC(BgScroll);
private:
	void update(float dt);
	Sprite* _sprite[2];
	float speed;
	float tex_length;
};

#endif

BgScroll.cpp

#include "BgScroll.h"
BgScroll::BgScroll():
speed(60.0f)
{
	auto tex = Director::getInstance()->getTextureCache()->addImage("taiko-bg.png");
	tex_length = tex->getPixelsWide();
	for (int i = 0; i < 2; ++i)
	{
		Sprite* node = Sprite::createWithTexture(tex);
		node->setAnchorPoint(Vec2(0, 0));
		this->addChild(node);
		node->setPosition(i*Vec2(tex_length, 0));
		_sprite[i] = node;
	}
	this->scheduleUpdate();
}
BgScroll::~BgScroll()
{

}

void BgScroll::update(float dt)
{
	for (int i = 0; i < 2; ++i)
	{
		auto node = _sprite[i];
		node->setPositionX(node->getPositionX() - speed*dt);
	}
	float x1 = _sprite[0]->getPositionX();
	float x2 = _sprite[1]->getPositionX();
	if (x2 < 0.0f)
	{
		_sprite[0]->setPositionX(x2 + tex_length);
		std::swap(_sprite[0], _sprite[1]);
	}
}

使用方法

 auto _bg_scroll = BgScroll::create();
    this->addChild(_bg_scroll);

总结

在游戏里面的任何动作,都需要*时间,因为每一帧的时间不是固定的,以前在某段时间会卡顿一下。

bgscroll

flightgear vs2013

本文描述flightgear vs2013编译调试可运行。以前也写过fg3.4的编译,如果想去看的请参考http://www.freeyun.com/flightgear-3-2-0-generation-debug.html

最早的fg2.6的编译已删除,因存在一定的误导性。适用3.4以上面的版本,注意看osg要求的版本。

必备软件

下载安装解压cmake、osg、3rdParty:http://pan.baidu.com/s/1qW9WOES

如果想自己编译osg的参考osg编译

新建环境变量OSG_DIR  D:Program Files (x86)OpenSceneGraph

flightgear simgear 开发的版本:http://sourceforge.net/p/flightgear/_list/git?source=navbar

flightgear simgear 稳定的版本:http://www.flightgear.org/download/source-code/

编译的目录说明

fg-dir

3rdParty:第三方库

flightgear-build:FG的生成vs2013目录

simgear-build:SG的生成vs2013目录

simgear及flightgear:源代码目录

bin:里面放了3rdParty的bin,和osg的bin目录,同时也需要指定fgfs.exe,比如我指定的vs2013的工作目录D:FlightGear-devbin。还有一种方法就是吧3rdParty和osg两者bin都添加到环境变量Path。

data:顾名思义就是fg的资源目录,你可以下载我提供的,那个是当前最新仓,当然你sim和fg也得是最新仓的,解压出来后,可以直接在我上面更新,省下很多时间,如你用了指定版本的,可以用安装版的data。

terrasync-dir:这个目录是空的,用于存放联网下载更新机场地景等等目录。

关于其他目录,不在这个范围之内就不解释。

Cmake简单三步

点击configure->选择编译器->点击Generate

编译simgear

修改simgearsimgearprops下面的PropertyInterpolationMgr.cxx函数

void PropertyInterpolationMgr::update(double dt)

,这个是一个bug,操作容器出错,

void PropertyInterpolationMgr::update(double dt)
  {
    if( _rt_prop )
      dt = _rt_prop->getDoubleValue();

	for (InterpolatorList::iterator it = _interpolators.begin();
		it != _interpolators.end();
		)
	{
		for (double unused_time = dt;;)
		{
			PropertyInterpolatorRef interp = it->second;
			unused_time = interp->update(*it->first, unused_time);

			if (unused_time <= 0.0)
				// No time left for next animation
			{
				++it;
				break;
			}
			if (interp->_next)
			{
				// Step to next animation. Note that we do not invalidate or delete
				// the current interpolator to allow for looped animations.
				it->second = interp->_next;
				++it;
			}
			else
			{
				// No more animations so just remove it
				it = _interpolators.erase(it);
				break;
			}
		}
	}
}

source code:D:/FlightGear-dev/simgear

the binaries:D:/FlightGear-dev/simgear-build

CMAKE_INSTALL_PRE:d:/Program Files (x86)/SimGear

打开sln文件, 编译INSTALL项目

新建环境变量SIMGEAR_DIR d:/Program Files (x86)/SimGear

编译flightgear

source code:D:/FlightGear-dev/flightgear

the binaries:D:/FlightGear-dev/flightgear-build

运行调试

设置fgfs启动项目并添加调试参数(选择fgfs->右键属性->调试->命令参数)

--fg-root="D:FlightGear-devdata" --geometry=800x600 --terrasync-dir="D:FlightGear-devterrasync-dir" --disable-ai-traffic --multiplay=out,10,127.0.0.1,5000 --multiplay=in,10,127.0.0.1,5001 --enable-terrasync  --log-level=debug

指定了data资源目录(如果按照目录结构也可以省掉fg-root参数) 分辨率 地景更新目录,关闭AI,联网的操作,然后你看你的网速了,会联网更新地景,进入到飞机画面,如果你想启动其他飞机,你也需要

--fg-aircraft=“飞机的目录名字(D:FlightGear-devdataAircraft)”比如 --aircraft="c172p"。

关于更多的参数请看手册。

最新飞机下载:https://svn.code.sf.net/p/flightgear/fgaddon/trunk/Aircraft/

图flightgear vs2013运行后

flightgear vs2013 737-100

 

总结

如果你发现出现问题了,请重复查看是否漏了那些步骤。

本文写的在后面,flightgear也一直在更新,更多的信息推荐去看wiki.flightgear.org,如果你对最新版本的感兴趣也去下载git仓,sf.net上,由于之前的git仓也已移到sf.net上面。如果你只需要制作飞机地景,就不需要编译FG,因为它有提供专门的工具制作,直接运行FG起来看效果,前提是你要知道怎么使用。作为开源的项目可能代码写的不好,可以去修改它。用的是3.7的版本号,已经有QT 5.0在里面。用来代替fgrun的那个图形启动界面。

 

flightgear联飞服务器fgms构建

本文描述关于flightgewar的联飞服务器fgms的vs2013构建及简要的分析下。

必备

fgms项目:https://gitlab.com/fgms/fgms-0-x

多线程库pthread win32:ftp://mirrors.kernel.org/sourceware/pthreads-win32/pthreads-w32-2-9-1-release.zip

fgms的配置文件http://flightgear.mxchange.org/fgms/fgms.conf

生成工程方案

打开cmake,fgms的目录下分别填入

source code:E:/fgms-0-x-master

build the binaries:E:/fgms-0-x-master/build

configure->选择vs2013->Finish。

设置pthread的路径

E:fgms-0-x-masterpthreadsPre-built.2include

E:/fgms-0-x-master/pthreads/Pre-built.2/lib/x86/pthreadVC2.lib

Generate生成解决方案->编译生成exe,拷贝pthreadVC2.dll到E:fgms-0-x-masterbuildDebug目录下。

复制文件fgms.conf到C:Usersfreeyun(备注这个是我的用户文件夹,紧供参考。)

fgms.conf这个我还域名深入去研究过它的参数,参数说明http://fgms.freeflightsim.org/fgms_conf.html。

源码简要分析

全部联机玩家的列表:m_PlayerList

//发送数据
int netSocket::sendto ( const void * buffer, int size,
                        int flags, const netAddress* to )
//接收数据
int netSocket::recvfrom ( void * buffer, int size,
                          int flags, netAddress* from )

写在后面

本来是想研究网络游戏的位置同步,不停的查找开源的项目,想起flightgear的项目,发现支持cmake生成。笔者只是初略的看源码,没深入,希望能给flighgear的研究的抛砖引玉,这里没写出联机设置,手册已经有了没必要在这里重复。

联机设置:http://wiki.flightgear.org/Howto:Multiplayer

更多的参考资料:http://fgms.freeflightsim.org/。

网站搬家

wordpress架的网站在外面好长时间,终于决定搬回来了,记录搬家到阿里云的过程。

原因

在美国的虚拟主机,便宜的,一年100多块,速度上还是接受,有每个月宽带限制,几个网站共用一个IP的,对于国内的虚拟主机价格实惠,做个小站流量小。通过DNSPOD监控发现有偶尔的无法访问,另外访问速度相对慢一点。在某些原因,写文章审核保存草稿比较慢,无法接受,通常我一篇文章写几个小时反复的查看,排版。上面种种原因,要搬到国内。

过程

1、购买云主机

通过对比选择阿里云,目前国内有腾讯云,盛大云,百度云等,价格上面阿里云优惠,还有是万网被阿里云收购,在云主机上面经历过几年的验证,稳定可靠性有保 证。另外我腾讯没512MB内存云服务器,我网站没有必要弄1GB内存云服务器。服务器地域青岛一个月49.5,在网上找个优惠码打9折。当然如果你想要1G内存的,也可以购买腾讯云。操作系统就选择linux centos 7.0。

2、备份数据:

一般在虚拟主机都有phpmyadmin,通过界面导出整个网站数据库。还有一种方法是利用wordpress工具->导出,来导出xml数据。不建议IE浏览器做这种操作,否则存在导出数据不全。拷贝你的wordpress程序。

3、安装环境

安装Xshell,登录到你的云主机。

用于相互传送文件的工具,运行:yum install lrzsz

lrzsz的命令说明 rz :本地文件到服务器,sz xx:服务器文件到本地。

安装lnmp(Nginx、PHP、MySQL、phpMyAdmin)命令:wget -c https://api.sinas3.com/v1/SAE_lnmp/soft/lnmp1.2-full.tar.gz && tar zxf lnmp1.2-full.tar.gz && cd lnmp1.2-full && ./install.sh lnmp

上面执行时间有点长,看你主机性能。

按照提示是在mysql密码。

4、恢复网站数据

添加vhost执行: lnmp add vhost  (注意选择支持伪静态)

删除是有的插件,防止某些插件导致网站502错误等。

4.1可视化恢复:

将你备份的程序拷贝至你的目录(比如我的www.freeyun.com)下,phpmyadmin创建数据库,恢复你的sql文件,或者你登录后台通过xml的文件恢复数据。

4.2mysql命令恢复,注意mysql命令后面的分号(;):

mysql -u root -p;//登录数据库

CREATE DATABASE IF NOT EXISTS wordpress DEFAULT CHARSET utf8 COLLATE utf8_general_ci;//创建数据库wordpress

show databases;//显示全部数据库
use wordpress;//选择数据库
source  /~/wordpress.sql;//导入数据文件
drop database wordpress;//销毁wordpress数据库

为了正常升级wordpess 及插件执行:chown www:www -R www.freeyun.com

目录www.freeyun.com 下的user.ini文件无法直接修改,而且是隐藏文件可能在winscp下可能无法看到,建议使用vim编辑器或nano编辑器进行修改。

如要修或删除需要先执行:chattr -i /网站目录/.user.ini

修改完成后再执行:chattr +i /网站目录/.user.ini

如果你想要访问你的网站,你需要wp_options下面siteurl的值修改成你服务器IP地址。

5、配置mail

为了你的主机支持wordpress mail的,即发表评论,你收到邮件通知,回复评论,人家评论者收到恢复。有两种方法

  • 安装WP-Mail-SMTP插件,
  • 安装postfix这个配置有点复杂,作为sendmail的代替者,速度秒发e-mail,我后面文章再补写一篇文章

6、备案

分两步审核,第一是阿里云审核,第二是你身份证属地通信管理局,具体就看阿里云的提示,我第一步3天时间(包括了邮寄资料的时间),第二步8个工作日。这期间,你要把你的dns指向阿里云IP。

结束语

2015-8-26通过的备案,备案期间,天天上去看管局通过没,接不到管局电话。经过几天观察,没发现有宕机,访问速度杠杠的,最近在由于我网站的图片有些多,是不是得用七牛的加速下下。gravatar 的大头像有点烦,国内是加载不到,禁用中。

TrinityCore MMORPG服务器设计分析

本文是通过TrinityCore研究从而对MMORPG服务器设计分析。

网络字节序

ByteBuffer是字节(unsigned char类型)处理,没有大小端处理。在网络通信应用程序上,这种是经常有的,网络是通过字节发送。

消息包格式:包头+包类型+包体

包头rc4加密

登录认证是(srp6)安全远程密码第六版协议,包类型是占一字节。

游戏中,包类型占1.5字节。

1、封包

重载&operator<<
按顺序把数据封包

2、解包

重载&operator>>
按顺序把数据解出

消息包处理

网络是采用boost asio

1、发送

这里需要注意,在这个执行,在多线程里面操作加锁

std::unique_lock<std::mutex> guard(_writeLock);

boost::asio的发送函数:_socket.async_write_some

1.1 登录认证的发送

void AuthSession::SendPacket(ByteBuffer& packet)
{
    if (!IsOpen())
        return;

    if (!packet.empty())
    {
        MessageBuffer buffer;
        buffer.Write(packet.contents(), packet.size());

        std::unique_lock<std::mutex> guard(_writeLock);

        QueuePacket(std::move(buffer), guard);
    }
}

 2.2在游戏中发送

void WorldSocket::SendPacket(WorldPacket const& packet)
{
    if (!IsOpen())
        return;

    if (sPacketLog->CanLogPacket())
        sPacketLog->LogPacket(packet, SERVER_TO_CLIENT, GetRemoteIpAddress(), GetRemotePort());

    ServerPktHeader header(packet.size() + 2, packet.GetOpcode());

    std::unique_lock<std::mutex> guard(_writeLock);

    _authCrypt.EncryptSend(header.header, header.getHeaderLength());

#ifndef TC_SOCKET_USE_IOCP
    if (_writeQueue.empty() && _writeBuffer.GetRemainingSpace() >= header.getHeaderLength() + packet.size())
    {
        _writeBuffer.Write(header.header, header.getHeaderLength());
        if (!packet.empty())
            _writeBuffer.Write(packet.contents(), packet.size());
    }
    else
#endif
    {
        MessageBuffer buffer(header.getHeaderLength() + packet.size());
        buffer.Write(header.header, header.getHeaderLength());
        if (!packet.empty())
            buffer.Write(packet.contents(), packet.size());

        QueuePacket(std::move(buffer), guard);
    }
}

2、接收

接收函数_socket.async_read_some

处理已收到数据包MessageBuffer& packet = GetReadBuffer();

登录认证接收处理:void WorldSocket::ReadHandler()

游戏过程处理:

void WorldSocket::ReadHandler()

bool WorldSocket::ReadHeaderHandler()

bool WorldSocket::ReadDataHandler()

流程

消息通过函数指针映射处理。

1、登录游戏

std::unordered_map<uint8, AuthHandler> const Handlers = AuthSession::InitHandlers();

2、认证通过后

void WorldSocket::HandleAuthSession(WorldPacket& recvPacket)

内部把这次玩家会话添加 :

新建一个会话_worldSession = new WorldSession(id, shared_from_this(), AccountTypes(security), expansion, mutetime, locale, recruiter, isRecruiter);

添加到世界:sWorld->AddSession(_worldSession);

判断是否有同个帐号登录,如果有,把它踢下线

2、全部会话处理:

void World::UpdateSessions(uint32 diff)
{
..
 for (SessionMap::iterator itr = m_sessions.begin(), next; itr != m_sessions.end(); itr = next)
..

}

2、游戏过程中

extern OpcodeHandler opcodeTable[NUM_MSG_TYPES]

写在后面

TrinityCore常用了TCP通信,tcp通信也是存在心跳的,网络是异常退出(比如,任务管理器直接杀死游戏进程),tcp协议不会通知说我已经退出。如果是实时再线格斗类型游戏,建议常用UDP。TrinityCore大量使用c++ 0x11,有些地方,我也不是明白,如果有存在错误的地方也请指出。

mmorpg魔兽世界服务器框架TrinityCore构建

本文对TrinityCore 版本7.x | 3.3.5编译说明通用。

简介

TrinityCore 是c++实现MMORPG框架

来自MaNGOS,大型网络对象服务,随着时间的推移,该项目代码广泛的优化,改善和清理代码。

这是完全开源的,社区参与是高度鼓励

如果你有好的想法请访问网站github 库https://github.com/TrinityCore/TrinityCore,提出。

更多TrinityCore项目信息,请访问TrinityCore.org

要求

  • 平台:Linux, Windows or Mac>
  • cpu支持SSE2
  • Boost ≥ 1.49
  • MySQL ≥ 5.1.0
  • CMake ≥ 2.8.11.2 / 2.8.9 (Windows / Linux)
  • OpenSSL ≥ 1.0.0
  • GCC ≥ 4.7.2 (Linux only)
  • MS Visual Studio ≥ 12 (2013) (Windows only)
  • 按里面提示的软件进行安装:http://collab.kpsn.org/display/tc/Requirements现在windwos的

安装

本节只讨论windows 7系统安装

TrinityCore:https://github.com/TrinityCore/TrinityCore/releases

1、生成vs2013工程解决方案

  1. 下载名字ource code (zip)
  2. 打开cmake
  3. Where is the Source code: E:/SDK/TrinityCore-stable
  4. Where to build th binaries:E:/SDK/TrinityCore-stable/build
  5. 点击Configure 现在 vs2013,把TOOLS勾上,点击Generate生成TrinityCore.sln
  6. 编译解决方案

如果是64位系统需要手动去把mysql库,指向win32的,否则出现link 2019错误。

2、生成地图

  1. 安装好World of Warcraft – 3.3.5a (12340) – enUS (No Install),建议编译Release的版本,生成地图快
    可C:\TrinityCore\contrib 复制 “extractor.bat”及地图生成工具4个exe 放wow.exe同目录,(2 3 4 5步骤可跳过)
  2. 在Wow.exe同目录下一次点击mapextractor.exe
  3. mmaps_generator.exe
  4. vmap4extractor.exe
  5. vmap4assembler.exe
  6. 生成的dbc maps mmaps vmaps拷贝v到 authserver.exe worldserver.exe目录下

3、数据库安装

  1. 默认mysql安装,添加帐号:trinity,密码:trinity
  2. 数据库图形管理工具HeidiSQL或者SQLyog,推荐用SQLyog
  3. 工具->现在执行sql文件…sqlcreatecreate_mysql.sql
  4. 选择auth数据库 工具->现在执行sql文件…sqlbaseauth_database.sql
  5. characters数据库 同上面操作执行…sqlbasecharacter_database.sql
  6. world数据库 同上面操作执行…”TDB_full_*.sql。下载名字是TDB_full

4、运行

  1. authserver.conf.dist和worldserver.conf.dist 去掉dist
  2. 运行authserver.exe,再运行worldserver.exe
  3. worldserver窗口里创建游戏帐号:account create username password
  4. 修改游戏客户端DataenUSrealmlist.wtf内容为:set realmlist 127.0.0.1
  5. 运行wow登录。。

结束语

大概步骤是和Mangos构建是类似的,上面没说到是用了那个版本,自己下载数据库和源码版本一致。

最后来个截图,这图只是研究使用,无意对暴雪侵权,图:魔兽世界:巫妖王之怒3.35

wow 3.35

Unity3d 使用boost asio在windows android

本文简单说明Unity3d c++插件,并采用boost asio打开串口设备状态在windows android平台下的交互通信。

Unity3d插件简介

Unity3D底层是c++实现的,用户用c#进行开发,不过是用mono开源,开源的 .NET 开发框架。在实际应用中,我们总有一些代码是其他编程语言实现,历史遗留。插件可以是C、C++、Objective-C等语言实现的。

构建插件

1、插件代码

1、百度云盘:里面有SimplestPluginExample-4.0和boost asio打开串口的例子http://pan.baidu.com/s/1mgy47jU

2、目录jni的MyClass.h是关于boost asio打开串口设备代码

3、下面代码来自官方SimplestPluginExample-4.0

#if _MSC_VER // this is defined when compiling with Visual Studio
#define EXPORT_API __declspec(dllexport) // Visual Studio needs annotating exported functions with this
#else
#define EXPORT_API // XCode does not need annotating exported functions, so define is empty
#endif

// ------------------------------------------------------------------------
// Plugin itself


// Link following functions C-style (required for plugins)
extern "C"
{

// The functions we will call from Unity.
//
const EXPORT_API char*  PrintHello(){
    return "Hello";
}

int EXPORT_API PrintANumber(){
    return 5;
}

int EXPORT_API AddTwoIntegers(int a, int b) {
    return a + b;
}

float EXPORT_API AddTwoFloats(float a, float b) {
    return a + b;
}

} // end of export C block

2、构建Windows插件

打开SimplestPluginExample-4.0SimplestPluginExampleVS2008PluginVisualStudioASimplePlugin.sln

3、构建Android插件

  1. 本机环境windows7
  2. 下载NDK,将Androidandroid-ndk-r9d加到系统环境变量
  3. 进入目录Unity3dPlugin-boostAsioboost-asio
  4. 执行:ndk-build

注意:一定要动态libgnustl_shared.so

 Unity3d插件boost asio应用

1、拷贝库

将编译好的dll和so文件放对应入AssetsPlugins目录下的Android、x86

2、添加到unity3d工程代码

using System.Runtime.InteropServices;
[DllImport("paokunet")]
private static extern void startNet();
[DllImport("paokunet")]
private static extern void stopNet();
[DllImport("paokunet")]
private static extern bool getNetStats();

 3、windows应用

boost库地址:http://sourceforge.net/projects/boost/files/boost-binaries/

没什么好说的

4、安卓应用

  • boost 安卓库 里面有说明编译,如不想看就结尾有现成库。
  • unity3d导出安卓项目记得导出地方对google android打勾
  • boost asio是因动态加载stl才能工作正常

1、添加一个java文件类似这样的

import com.unity3d.player.UnityPlayerActivity;

import android.os.Bundle;
import android.util.Log;

public class OverrideExample extends UnityPlayerNativeActivity {
    static
    {
        //加载.so文件
      Log.d("OverrideActivity", "paokunet freeyun ???????");
      {
          System.loadLibrary("gnustl_shared");
          System.loadLibrary("paokunet");
       }
    }
}

2、AndroidManifest.xml类似这样

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.Unity3d.u3d" android:versionName="1.0" android:versionCode="1" android:installLocation="auto">
  <supports-screens android:smallScreens="true" android:normalScreens="true" android:largeScreens="true" android:xlargeScreens="true" android:anyDensity="true" />
  <application android:theme="@android:style/Theme.NoTitleBar" android:icon="@drawable/app_icon" android:label="@string/app_name" >
    <activity android:label="@string/app_name" android:screenOrientation="landscape" android:launchMode="singleTask" android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|orientation|screenLayout|uiMode|screenSize|smallestScreenSize|fontScale" android:name="com.hj.u3d.OverrideExample">
      <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
        <category android:name="android.intent.category.LEANBACK_LAUNCHER" />
      </intent-filter>
      <meta-data android:name="unityplayer.UnityActivity" android:value="true" />
      <meta-data android:name="unityplayer.ForwardNativeEventsToDalvik" android:value="false" />
    </activity>
  </application>
  <uses-sdk android:minSdkVersion="9" android:targetSdkVersion="22" />
  <uses-feature android:glEsVersion="0x00020000" />
  <uses-feature android:name="android.hardware.touchscreen" android:required="false" />
  <uses-feature android:name="android.hardware.touchscreen.multitouch" android:required="false" />
  <uses-feature android:name="android.hardware.touchscreen.multitouch.distinct" android:required="false" />
</manifest>

 总结

只是用用boost aiso串口代码,注意串口的设备打开是不一样的,安卓和Linux设备类似“”/dev/ttyS1”,windows类“COM8”如果需要用tcp udp通信也是可以的。