processInput
对于Input的理解应该非常开放。前面说的timerEvent是输入,客户端的鼠标键盘游戏杆的操作是输入,网络上来的信息也是输入。基本上,系统中发生的所有事件,技术上都可以处理为输入。对于服务器来说,它的输入主要来自于网络,因为它通常是个守护进程。对于客户机来说,输入主要来自于玩家的操作和服务器的同步消息。
ProcessInput方法可以直接处理所有的输入,调用相应的事件响应函数,也可以只做消息分派的工作,将消息发送到物件的事件队列中,让物件在updateScene阶段自行处理消息。
updateScene
这个方法是一个Frame Activity中最重要的阶段。它更新所有物件的状态,为一个帧的渲染作好准备。为了更好地隔离框架和应用,我们将状态的更新放在每个物件自己身上。在面向对象的模式下,我们可以为每个物件实现一个接口,让UpdateScene方法依次调用各个物件的该接口更新自己。更好的方法,是使用脚本语言,为每一类物件配置一个脚本,然后在UpdateScene方法中依次调用各个物件的脚本。这种方式在根本上可接口方式是一致的,不过使用了脚本,使得游戏应用的维护变得更加简单。
GameApp::updateScene(){
ObjectList objects = m_sceneManager.getObjects();
for(Object obj=objects.first(); !obj.isNull(); obj=objects.next()){
obj.run(...); // Or obj.doUpdateScript();
}
}
renderScene
一帧的最后阶段就是进行场景的渲染了。对于服务器和客户机来说,渲染的动作是不一样的。对于服务器,渲染是指将上一帧到这一帧发生的事件(根本上就是场景状态的改变)同步给感兴趣的客户端。对于客户端,渲染是指将当前帧的状态渲染到客户机的屏幕上。客户端的渲染,主要是图形渲染。(在文字MUD时代,渲染是文字的)
图形渲染的问题,我们会在后面讲到。这里对服务器渲染多说几句。因为服务器的渲染,就是所谓的场景同步了。
简单地讲,在updateScene的时候,每个对象调用run方法,将状态改变的同时,也将状态改变的消息压到服务器的同步消息列表中。到了rennderScene阶段,服务器读取服务器的同步消息列表,根据消息的LoD大小,根据当前场景状态,再将消息分发到感兴趣玩家的同步消息列表中。最后服务器遍历玩家列表中的每个玩家,并执行相应的接口发送同步消息。
GameApp::renderScene(){
m_sceneManager.dispatchEvents();
PlayerList playerList = m_scenceManager.getPlayerList();
for(Player player=playerList.first();!player.isNull();player=playerList.next()){
player.renderEvents();// Or player.doRenderScripts(); a player is a proxy of a client machine
}
}
renderEvent通过调用网络引擎将消息广播出去。
回到Frame概念
在游戏编程中,特别重要的一个概念是,一个场景的变化或者一个玩家的动作,通常不是立即完成的,而是在事件轴上通过无数帧来完成的。比如一个玩家从一个位置移动到另一个位置这个动作。player.moveTo(newPosition, velocity); 这样的一个调用并不直接将玩家搬移到目标位置,而只是给玩家对象下达了一个指令,比如说让他进入移动状态,并设置目标和速度。玩家真正的移动过程,是在每一帧里面计算完成的。不象一般的商用软件,一个调用完成就基本代表一个操作完成。这是游戏程序和一般的商用软件区别最大的地方。
一定要理解,游戏是在时间轴上进行的。它的根本驱动力,是时间。就好像放电影,一个举手的动作,是通过无数静态的图象连续播放完成的。游戏程序的任务,就是要根据当前的剧情(场景状态)来控制管理要播放哪一帧图象。
同步问题的收尾
理解了游戏程序的基本架构,同步的问题也就清楚了。有几个细节问题,再稍微讲一下,就差不多了......
