python碰撞检测算法_使用Python进行精确的游戏事件 (一)

3.5 使用Python进行精确的游戏事件广播

Matthew Walker,NCsoft公司

mwalker@softhome.net

年来,事件驱动系统一直被用来实现现代软件所要求的高度交互。游戏设计人员不仅可以使用这些系统来模拟并发,还可以用它们来提高软件对非预期输入迅速作出反应的能力。正如大多数商业软件中所提到的,事件驱动系统通常用于图形用户界面(Graphical

User

Interfaces,GUI)的开发。随着计算机游戏从简单的DOS程序发展到复杂的三维虚拟现实引擎,事件驱动系统对于游戏开发来说越来越重要。在MMP游戏中,事件将会空前重要,因为大量的用户会导致在任意时间都有很多事情发生。

本文介绍了3种不同的模式,它们使用Python程序设计语言,以在MMP服务器中实现高级游戏事件系统,每一种都比前一种复杂。本文的源代码可以在所附的CD-ROM中找到。每种模式都建立在前一种模式的基础上并且使系统变得更为强大和灵活。与此同时,本文会对每种实现的优缺点进行讨论。第3种(也就是最终的)模型中所使用的技术能够精确地控制事件的分发和处理,它可以用来创建一个非常灵活有效的框架并使用在不同的游戏系统中。

3.5.1 事件驱动编程

事件驱动编程(event-based

programming)这一术语是指不需要使用线程就可以在一个进程内模拟并发的技术。线程是一种管理并发的通用系统,它在操作系统这一级实现。要正确地使用线程非常困难,因为代码执行权在任何时刻都可能会被抢占。使用线程不仅需要对共享数据进行专门的管理,调试起来也很麻烦,更不用说它们通常要求类和模块能够正确地处理对时间的依赖,而这往往会违反抽象规则。与此相反,事件是非抢占式的,它们只有在被应用程序调用时才会执行,并且会持续执行直到任务完成。这使得事件驱动编程更容易理解和调试,并且可以在代码中合适的地方对时间依赖进行分离[Ousterhout96]。

1.同步和异步调用

事件驱动编程依赖于对两个互补概念的理解:同步(synchronou)和异步调用(asynchronous

call)。同步调用(也被称为阻塞(blocking)调用)在执行完毕之前不会把控制权返回给调用者。同步操作的调用者可以认为

当调用返回时,它所请求的操作已经执行完毕。与此相反,从调用者的角度来看,异步调用(也称为非阻塞(non-blocking)调用)会立即返回,但是它们会独立于调用者的执行而继续执行。异步操作的调用者必须作一些特殊安排从而在异步调用完成后能够收到通知。通常,游戏开发人员可以使用回调函数(callback

function)的方式,异步操作会在完成任务后调用这个函数。

2.并发模拟

在MMP游戏服务端架构中提供一个并发的假象是至关重要的,因为游戏服务端必须随时为游戏中成千上万的玩家提供支持。事件驱动系统通过把每个玩家的请求当作一个小型原子操作来实现这点。请求会随着接收的顺序被依次执行。如果一个复杂操作可以被安全地分解为两个或多个异步操作,它就会被分发给事件系统并在游戏循环下一次执行时被处理。因为完成每个请求所需要的时间都很短,这样服务端就可以很快地对新的请求进行处理,这就提供了一个假象:多个请求被同时并发地处理了。

3.高级游戏事件

本文着重讨论了怎样使用基于事件的方法来处理高级(high-level)游戏操作。这包括大多数由玩家请求直接调用的功能,譬如说与游戏世界中的物品、其他玩家或是关键的游戏系统进行交互。这还包括了由AI控制的角色发出的类似行为。但是它不包括那些像底层网络代码、物理系统、碰撞检测、移动控制之类的功能。然而,这些底层系统可以为事件驱动的游戏代码提供钩子(hook)。譬如说,碰撞系统可以在一个对象接触到一个碰撞体(collsion

volume)的时候调用游戏代码注册的回调函数。这样的钩子可以被简单地看作事件。

4.游戏服务端主循环

游戏服务端的主循环是事件驱动系统的根(root)。它非常简单,正如下面的伪码所示。

mainloop()

{

while(game_is_running)

{

// wait for incoming requests

WaitForRequests();

// handle requests

ProcessRequests();

}

}

WaitForRequests(等待请求)函数可以让服务端进入一个有效的等待状态,直到收到某个请求。要实现这个功能,游戏服务端可以在I/O资源(譬如说网络套接字和内存文件、I/O完成端口或是异步函数调用(APC)[MSDN01])上使用select(),也可以循环地调用sleep()函数并检查输入队列中有没有新的数据。

ProcessRequests(处理请求)函数接收任何等待请求并且把它们分发给合适的代码来进行处理。请求可以包含从游戏客户端收到的玩家请求,也可以包含游戏循环某次执行中所做的异步调用。分发可以是对回调函数的直接调用,也可以是根据请求中的某些输入进行由数据驱动的委派。这一功能的关键在于所需执行的代码可以预先知道,并且只需要很少的判断就可以决定应该执行哪些代码。

5.事件和线程并不是互斥的

在游戏服务端使用事件驱动的方式与在系统中正确地使用线程并不冲突。上面所介绍的游戏循环可以运行在一个多线程服务器中,它可以在一个独立的游戏线程中运行,而由其他线程对网络消息、初始化系统、关闭系统等进行处理。网络线程可以把接收到的请求放入一个队列,游戏线程从这个队列中取出请求并进行处理。游戏循环中发出的异步调用也可以插入这个请求队列,无论是否还有其他线程存在。

6.Python和事件驱动编程

几乎所有程序设计语言都可以实现事件驱动编程。由于以下理由,本文中的示例都使用Python实现。

1.它的语法和语义很容易理解。

2.它是动态类型的语言,可以创建复杂的数据结构而不会受到由此带来的类型限制。

3.在Python中,类、模块、函数和方法都是头等对象(first-class

object),这样可以很方便地实现回调和其他事件驱动方案。

4.在Python中,类、模块和实例的属性都可以通过一个字符串名字来访问,因此游戏设计人员可以更灵活地定义延迟绑定(late-bound)的函数调用来实现事件驱动。

3.5.2 延迟调用

本文所介绍的第一个事件驱动系统是基于延迟调用(deferred

call)模式的。这是一个非常常见的基本用法,所有后续的实现都以它为基础。延迟调用就是指使用请求的方式来对函数或者方法进行调用,从而使它们可以在未来的某个时间被处理。在事件驱动系统中,这意味着这个请求将会在游戏循环的某次后续执行中得到处理。延迟调用具有6个主要元素:

1.调用的目标对象(target object);

2.调用本身;

3.调用的参数,它必须符合调用的形式参数列表;

4.调用的执行时间,这必须是将来的某个时间;

5.当延迟调用完成后所调用的回调函数;

6.回调函数的参数。

1.延迟调用接口

延迟调用实现使用了下面的定义。

def Call(target,call,args,delay,cb,cbArgs):

#

deferred call implementation

目标对象(target)参数是一个整型的对象标识。使用一个对象管理器类就可以把它转化为对象引用。因为这个调用是异步的,在执行到它的时候,这个目标对象可能已经被游戏销毁了。如果使用目标对象的引用而不是标识来作为参数,可能会影响这个对象的清除工作,因为Python是基于引用计数的[Python01]。

调用(call)参数是一个字符串,它记录了目标对象中服务端想要调用的方法的名字。它使用Python的getattr()函数来通过名字查找要调用的方法。因为Python的方法是头等对象,它们也会被引用计数,这个方法避免了上面所说的相同的问题。

调用参数(args)参数是一个元组(tuple),它包含了这个调用的形式参数,这些形参必须符合这个调用的参数列表。元组是Python的基本数据类型,它本质上是一个不可改变的列表(immutable

list)。

延迟时间(delay)参数表示进行这个延迟调用之前所需等候的最小时间,它的单位是毫秒。这个值也可以是0;这意味着这个延迟调用会立刻进入请求队列。

回调函数(cb)参数是对某个回调函数或方法的引用,它会在延迟调用完成后被调用。如果不需要通知,可以把它设为None(Python中的NULL类型)。这里接受一个引用是可行的,因为发出调用的代码应该知道它自己的生命期,可以假设如果它知道自己不能接收到这个通知,它就不会提出要求。

回调函数参数(cbArgs)参数也是一个元组,它包含了传递给回调函数的参数。如果不需要任何回调函数,也可以把它设为None。如果一个回调函数不需要任何参数,就可以使用一个空元组“()”,这样可以避免对这个值进行额外的检查。

2.延迟调用的实现

在deferred.py模块中可以实现这些功能。这些功能的实现需要满足两个关键要求。

1.延迟调用必须能被缓存在某种类型的容器里,并且可以在以后取出。

2.延迟调用不能在它们所期望的执行时间之前执行,如果多个延迟调用被安排在同一时间执行,它们将按照先进先出的方式被处理。

因此,这些功能的实现不仅需要某种形式的优先队列,还需要某种方式来根据一个调用的调度时间来决定其优先级。第二个问题很有趣,它需要我们定义一个可以按照时间来排序的“可调用对象(callable

object)”。下面是一个可行的实现。

import objMgr # object manager: maps ids to objects

class DeferredCall:

def

__init__(self,id,call,args,t,cb,cbArgs):

self.targetId = id

self.cal· = call

self.args = args

self.time = t

self.callback = cb

self.cbArgs = cbArgs

def

__cmp__(self,other):

return cmp(self.time,other.time)

def

__call__(self,objMgr):

target = objMgr.GetObject(self.targetId)

if target is not None:

try:

method = getattr(target,self.call)

except AttributeError:

print "No %s on %s" % (self.call,target)

return

apply(method,self.args) # make the call

if self.callback is not None:

# notify that call is complete

apply(self.callback,self.cbArgs)

这个类对延迟请求的重要属性进行了封装。导入对象管理器(objMgr)模块就可以根据对象标识获得目标对象。__init__()构造函数使用请求数据来构造对象。注意在self.time属性中包含了一个从Call()函数的延迟时间参数计算而来的绝对时间值。这个转换是为了支持在优先级队列中对延迟调用(DeferredCall)对象进行排序。要进行这样的排序,程序员必须使用Python保留的__cmp__()钩子[Python02],它使得对对象进行比较时可以以它们的执行时间(time)属性为依据。通过另一个Python保留的钩子__call__()方法就可以使得延迟调用对象可以像函数/方法那样被调用。这个方法先获得目标对象和以它为对象进行调用的方法,然后再进行调用,如果存在回调函数,它还会接着执行回调函数。无论是调用目标对象的函数还是调用回调函数,都要用到Python的apply()函数[Python03],它使得我们可以把参数作为元组来传递给函数从而避免受到目标函数参数数量的限制。

Call()函数的实现使用了一个延迟调用对象,如下所示。

import time

import

bisect #

efficient list operations

# module-scoped list

deferredCalls = []

def Call(target,call,args,delay,cb,cbArgs):

#

schedule a call for later execution

callTime

= time.time() + float(delay) / 1000.0

dCall =

DeferredCall(target,call,args,callTime,cb,cbArgs)

bisect.insort_right(deferredCalls,dCall)

这里的实现是基于模块的,而不是基于类的。这纯粹是出于服务端架构的要求,它并不会影响游戏原理。这个模块定义了一个延迟调用(deferredCalls)列表,所有的延迟调用都会被插入这个列表并且按照时间排序。Call()函数会把延迟时间(delay)参数转换为一个绝对时间,随后创建一个延迟调用对象并把它插入到延迟调用列表中。Python的bisect模块中的insort_right()函数可以使用游戏开发人员为延迟调用类所定义的__cmp__()方法来进行一个高效的插入排序。

现在来实现执行延迟调用的代码。这里使用的基本算法是枚举延迟调用列表中的元素,并且执行每个执行时间属性小于或等于当前时间的延迟调用对象。在完成相应的操作后,延迟调用对象会调用回调函数。因为这个列表是经过排序的,所以可以在遇到第一个执行时间属性晚于当前时间的延迟调用对象时跳出循环。

def ExecuteDeferredCalls():

# run

deferred method calls

dCall =

None

now =

time.time()

while

1:

dCall = deferredCalls.pop(0) # front of the list

if dCall.time > now:

# not time yet,put it back at front of the list

deferredCalls.insert(0,dCall)

break

dCall() # execute the call,and any callbacks

# now

return how long in ms until next call

next =

None #

forever

if dCall

is not None:

next = int((dCall.time - time.time()) * 1000.0)

if next < 0:

next = 0

return

next

ExecuteDeferredCalls()(执行延迟调用)函数会返回在执行下一个延迟调用前还需要等待的毫秒数。可以把这个作为像select()一样的异步等待函数的参数。

3.进行一次延迟调用

进行一次延迟调用几乎和进行一次普通的方法调用一样方便。主要的区别在于是怎样指定所调用的对象和方法的。此外,游戏的程序编写人员还必须决定是否提供一个回调函数来接收通知。

import deferred

def DoSomething():

objId =

42 # a parrot,Norwegian Blue

intensity

= 9

sincerity

= 0

deferred.Call(objId,'WakeTheParrot',

(intensity,sincerity),100,Callback,(objId,))

def Callback(objId):

print

'Parrot %d is pining for the fjords. ' % objId

上面的例子说明了如何向42号对象(这是一只挪威蓝鹦鹉)发出一次延迟调用。WakeTheParrot(唤醒鹦鹉)方法会在100毫秒以后被调用,强度(intensity)为9,真实度(sincerity)为0。这个操作完成后,鹦鹉的对象标识会被作为参数来调用回调函数,“Parrot

42 is pining for the fjords.(第42号鹦鹉非常向往海湾)”被打印出来。

4.在主循环中调用

只需要参考最初的游戏循环伪码,就可以理解这些函数在事件驱动系统中的作用。WaitForRequests()函数会保持阻塞,直到它从客户端接收到一个请求或者下一个延迟调用的延迟时间过去了。ProcessRequest()函数调用ExecuteDeferredCalls(),它会执行所有可以执行的延迟调用,并且返回下一次延迟调用前所需等待的时间。如果要让ProcessRequests()把这个时间值返回给主循环,可以对主循环的伪码作如下修改:

mainloop()

{

waitTime

= 0;

while(game_is_running)

{

// wait for incoming requests

WaitForRequests(waitTime);

// handle requests

waitTime = ProcessRequests();

}

}

5.延迟调用带来的影响

延迟调用是为游戏服务器实现事件驱动系统的一个简单而灵活的方法。它的优势在于它容易理解和实现,并且适用于任何形式的异步调用。然而,它也有一些特有的限制。首先,调用方必须知道被调用的目标对象的标识,这在游戏这样的动态环境中会带来一些限制。其次,游戏中的事件往往会导致一些动作的执行,这些由给定事件发起的动作通常会随着游戏状态的改变而改变。要使用延迟调用模式,游戏的开发人员必须使用某些状态机从而在任何时刻都可以发出特定的调用。这可能需要加入复杂的条件代码,甚至连调试和维护都会变得更为困难。不仅如此,有些新的功能需要对给定的事件作出反应,加入这些新功能后,调用代码也必须被修改。因为延迟调用可以在游戏代码中的任何一点进行,无法找到一个独立的点来进行修改或调试错误。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值