Python游戏引擎开发(五):Sprite精灵类和鼠标事件
2016-02-03 21:14
846 查看
本次来实现
说起这个
那也许你会想,为什么要有这个类呢?举个例子大家就明白了。在一款RPG游戏中(如:口袋妖怪),我们的地图上有树林、小河等一系列地图元件。玩过此类游戏的同学都知道,如果我们的人物走到了地图中央继续前进的话,地图会进行卷轴移动,显示出下部分地图。这个时候我们如果要把每个地图元件进行移动,操作起来会相当麻烦。因此flash为我们提供的
经过上面的介绍,大家可能仍然无法理解这么抽象的一个类。那姑且把它视作一个层吧,我们可以通过
以下是前面章节目录:
Python游戏引擎开发(一):序
Python游戏引擎开发(二):创建窗口以及重绘界面
Python游戏引擎开发(三):显示图片
Python游戏引擎开发(四):TextField文本类
可以看到,这个类的实现代码很简单,就是添加了显示列表属性(childList)、鼠标事件列表(mouseList)和添加/删除对象的方法。实际上在flash中,这个类还有很多功能,比如说以后会提及的矢量绘图。看过第二章的同学应该会注意到
鼠标事件大致传递过程如下:
Created with Raphaël 2.1.0点击窗口QWidget窗口部件舞台stage对象直接addChild到stage上的一级Sprite一级Sprite的子对象二级Sprite(一级Sprite中的Sprite)的子对象……
为了给
主要是重写了
值得注意的是,
我们在使用鼠标事件时,多数情况下要获取鼠标的位置,所以我们将鼠标信息也记录下来,记入变量
offsetX,offsetY:鼠标相对于屏幕左上角的位置
eventType:鼠标事件类型
target:鼠标所点击的显示对象,初始值为
最后进入
在这个方法中,首先接受两个参数,一个就是鼠标信息,另一个是坐标对象(包含x,y坐标,scaleX,scaleY拉升值)。为什么要弄个坐标对象呢?因为我们在判断显示对象是否被点击时,需要用到坐标计算,而进行坐标计算时,需要获取对象的绝对位置,这个坐标对象就是用于计算绝对位置用的。随着事件往下级对象的传递,坐标对象会作为上级对象的坐标数据往下级对象传递,从而进行递归式坐标计算,节省效率。
在其中,我们遍历了所有底层子对象并且判断是否可以进入鼠标事件向下级子对象循环,如果可以(及判断有无
一般情况下,有
和Stage中的
这个方法的具体功能,如图所示:
![](https://img-blog.csdn.net/20160203204818643)
对Object B使用该方法,那么传入的origin参数相当于Object A的坐标信息,参数obj就是Object B,返回的对象中,x属性就是90,y属性就是70。
虽然
以上代码还是很好理解的,至于
对于
和其他显示对象不同的是,它通过遍历子对象,并调用它们的
还有一个
该方法中,首先是遍历了鼠标事件列表,找到对应的事件,然后触发事件监听器(即回调函数)。注意,监听器接受一个参数,该参数是一个
最后添加加入事件方法
最后加入
使用时,这么写就可以了:
运行截图如下:
![](https://img-blog.csdn.net/20160203211334049)
至此,我们就把
预告:下一篇我们实现动画类。
欢迎大家继续关注我的博客
转载请注明出处:Yorhom’s Game Box
http://blog.csdn.net/yorhomwang
Sprite类和鼠标事件。
说起这个
Sprite啊,涉及过2D游戏研究领域的看官应该都听说过它。它中文原意是“精灵”,不过在不同人的眼中,它所表示的意义不同。比如说在cocos2d中,它可以是一张图片。不过在flash中,
Sprite是一个类似于“层”的家伙。当然你把它定义为层并不是很准确,实际上它是一个含显示列表的显示对象。什么意思呢?各位看官如果阅读了前面的章节,那对显示列表并不陌生。它说白了就是一个包含其他显示对象的容器。
那也许你会想,为什么要有这个类呢?举个例子大家就明白了。在一款RPG游戏中(如:口袋妖怪),我们的地图上有树林、小河等一系列地图元件。玩过此类游戏的同学都知道,如果我们的人物走到了地图中央继续前进的话,地图会进行卷轴移动,显示出下部分地图。这个时候我们如果要把每个地图元件进行移动,操作起来会相当麻烦。因此flash为我们提供的
Sprite就是为了统一处理一系列显示对象而生的。
经过上面的介绍,大家可能仍然无法理解这么抽象的一个类。那姑且把它视作一个层吧,我们可以通过
Sprite的
addChild来向这个层添加显示对象。添加进去的对象所进行的操作都是相对的,比如说移动,旋转。
以下是前面章节目录:
Python游戏引擎开发(一):序
Python游戏引擎开发(二):创建窗口以及重绘界面
Python游戏引擎开发(三):显示图片
Python游戏引擎开发(四):TextField文本类
Sprite的实现
以下是实现代码:class Sprite(DisplayObject): def __init__(self): super(Sprite, self).__init__() self.childList = [] self.mouseList = [] def addChild(self, child): self.childList.append(child) def removeChild(self, child): self.childList.remove(child) child.parent = None def _loopDraw(self, c): stage._showDisplayList(self.childList)
可以看到,这个类的实现代码很简单,就是添加了显示列表属性(childList)、鼠标事件列表(mouseList)和添加/删除对象的方法。实际上在flash中,这个类还有很多功能,比如说以后会提及的矢量绘图。看过第二章的同学应该会注意到
stage._showDisplayList这个方法,他负责遍历显示列表并显示遍历得到的对象(及子对象)。由于这个方法是在
QPainter变换(平移,旋转,拉伸)之后,
QPainter.restore()之前被调用的,所以再次调用到这个子对象显示方法时,显示方法中对
QPainter的变换就是相对于先前
QPainter变换而言的。因此,我们就实现了子对象相对父对象变换的效果。
鼠标事件
我们要来实现鼠标事件方面的功能了。首先需要了解的是,由于我们无法直接对我们写的显示对象添加事件,所以只能先对QWidget添加鼠标事件然后在进一步进行计算来判断是否触发到我们的事件。
鼠标事件大致传递过程如下:
Created with Raphaël 2.1.0点击窗口QWidget窗口部件舞台stage对象直接addChild到stage上的一级Sprite一级Sprite的子对象二级Sprite(一级Sprite中的Sprite)的子对象……
为了给
QWidget添加鼠标事件,我们需要改动
CanvasWidget类:
class CanvasWidget(QtGui.QWidget): def __init__(self): super(CanvasWidget, self).__init__() self.setMouseTracking(True) def paintEvent(self, event): stage._onShow() def mousePressEvent(self, event): self.__enterMouseEvent(event, "mouse_down") def mouseMoveEvent(self, event): self.__enterMouseEvent(event, "mouse_move") def mouseReleaseEvent(self, event): self.__enterMouseEvent(event, "mouse_up") def __enterMouseEvent(self, event, eventType): e = {"offsetX" : event.x(), "offsetY" : event.y(), "eventType" : eventType, "target" : None} stage._enterMouseEvent(e, {"x" : 0, "y" : 0, "scaleX" : 1, "scaleY" : 1})
主要是重写了
QWidget中的几个事件回调(
mouseReleaseEvent,
mouseMoveEvent,
mousePressEvent)以及添加事件进入显示对象的入口
__enterMouseEvent,该函数的参数一个是Qt发来的事件对象,保存了一些事件信息,如鼠标位置,另一个是鼠标事件类型,比如”mouse_up”,”mouse_down”。
值得注意的是,
setMouseTracking方法是用于不停地触发移动事件,否则Qt默认只处理一次。
我们在使用鼠标事件时,多数情况下要获取鼠标的位置,所以我们将鼠标信息也记录下来,记入变量
e,这个变量将随着事件的传递,一直传下去。顺便对其中的属性进行说明:
offsetX,offsetY:鼠标相对于屏幕左上角的位置
eventType:鼠标事件类型
target:鼠标所点击的显示对象,初始值为
None,当事件传递到需要获取此属性的时候,就会被赋值为被点击的对象
最后进入
Stage._enterMouseEvent函数,将事件传递到舞台对象上。
Stage._enterMouseEvent的代码如下:
def _enterMouseEvent(self, event, cd): childList = self.childList[:: -1] currentCd = {"x" : cd["x"], "y" : cd["y"], "scaleX" : cd["scaleX"], "scaleY" : cd["scaleY"]} for o in childList: if hasattr(o, "_enterMouseEvent") and hasattr(o._enterMouseEvent, "__call__") and o._enterMouseEvent(event, currentCd): break
在这个方法中,首先接受两个参数,一个就是鼠标信息,另一个是坐标对象(包含x,y坐标,scaleX,scaleY拉升值)。为什么要弄个坐标对象呢?因为我们在判断显示对象是否被点击时,需要用到坐标计算,而进行坐标计算时,需要获取对象的绝对位置,这个坐标对象就是用于计算绝对位置用的。随着事件往下级对象的传递,坐标对象会作为上级对象的坐标数据往下级对象传递,从而进行递归式坐标计算,节省效率。
在其中,我们遍历了所有底层子对象并且判断是否可以进入鼠标事件向下级子对象循环,如果可以(及判断有无
_enterMouseEvent方法),则进行。除此之外,为了实现鼠标事件遮挡效果,我们特地的反着遍历显示列表,也就是说先遍历得到显示在上层的对象,调用这些对象的
_enterMouseEvent方法,该方法返回值若为
True则代表鼠标在该显示对象上面,通过
break中断遍历。
一般情况下,有
_enterMouseEvent的,大半是
Sprite对象。所以我们为
Sprite添加这个方法:
def _enterMouseEvent(self, e, cd): if not self.visible: return currentCd = self.__getVisualCoordinate(cd, self) isOn = self._isMouseOn(e, currentCd) if isOn: for o in self.childList[::-1]: if (hasattr(o, "_enterMouseEvent") and hasattr(o._enterMouseEvent, "__call__") and o._enterMouseEvent(e, currentCd)): break self.__dispatchMouseEvent(e, currentCd) return False
和Stage中的
_enterMouseEvent非常类似。其中用了子对象的
_isMouseOn方法,用于判断是否点击到该对象上。而
__dispatchMouseEvent用于触发鼠标事件。
__getVisualCoordinate用于得到一个显示坐标,这个好比我们看三维图形直观图,实际的大小和看到的不是一样的。由于显示对象的x,y坐标是相对父对象的,所以我们通过这个方法来实现得到看到的大小和位置,及相对于屏幕左上角的绝对位置。
__getVisualCoordinate代码:
def __getVisualCoordinate(self, origin, obj): return { "x" : origin["x"] + obj.x * origin["scaleX"], "y" : origin["y"] + obj.y * origin["scaleY"], "scaleX" : origin["scaleX"] * obj.scaleX, "scaleY" : origin["scaleY"] * obj.scaleY }
这个方法的具体功能,如图所示:
对Object B使用该方法,那么传入的origin参数相当于Object A的坐标信息,参数obj就是Object B,返回的对象中,x属性就是90,y属性就是70。
scaleX和
scaleY同理。
虽然
Sprite是一个显示对象,但是可见的其实是里面的
Bitmap,
TextField等子对象,所以我们在为这些显示对象添加
_isMouseOn方法时,要区分对待。对于
Bitmap等多数显示对象,我们采用判断点击的位置是否在显示对象所处的矩形范围内(毕竟我们几乎所有显示对象都是矩形的,暂时这么简单实现一下),给
DisplayObject类添加该方法:
def _isMouseOn(self, e, cd): if not self.visible: return ox = e["offsetX"] oy = e["offsetY"] x = cd["x"] y = cd["y"] scaleX = cd["scaleX"] scaleY = cd["scaleY"] w = self._getOriginalWidth() h = self._getOriginalHeight() if x <= ox <= x + w * scaleX and y <= oy <= y + h * scaleY: e["target"] = self return True return False
以上代码还是很好理解的,至于
_getOriginalWidth,
_getOriginalHeight二厮,不知道大家还记得不,是前面提到的获取显示对象原始宽高(忽略
scaleX和
scaleY)的方法。通过
if x <= ox <= x + w * scaleX and y <= oy <= y + h * scaleY:判断点击的位置是否在显示对象内。
对于
Sprite,添加不同的
_isMouseOn方法:
def _isMouseOn(self, e, cd): if not self.visible: return childList = self.childList[::-1] for o in childList: childCd = self.__getVisualCoordinate(cd, o) if o._isMouseOn(e, childCd): e["target"] = o return True return False
和其他显示对象不同的是,它通过遍历子对象,并调用它们的
_isMouseOn来完成判定鼠标是否盘旋在该
Sprite上。其中设置了
target属性,用于方便使用者获取点击对象。
还有一个
_dispatchMouseEvent方法,用于触发鼠标事件:
def __dispatchMouseEvent(self, e, cd): for o in self.mouseList: t = o["eventType"] l = o["listener"] if t == e["eventType"]: eve = object() eve.offsetX = e["offsetX"] eve.offsetY = e["offsetY"] eve.selfX = (e["offsetX"] - cd["x"]) / cd["scaleX"] eve.selfY = (e["offsetY"] - cd["y"]) / cd["scaleY"] eve.target = e["target"] eve.currentTarget = self l(eve)
该方法中,首先是遍历了鼠标事件列表,找到对应的事件,然后触发事件监听器(即回调函数)。注意,监听器接受一个参数,该参数是一个
object,储存了鼠标相对于屏幕左上角的坐标(
offsetX和
offsetY),以及相对于
Sprite对象的坐标(
selfX和
selfY),还可以通过
currentTarget属性获取触发事件的
Sprite对象,以及通过
target属性获取点击到的对象。
最后添加加入事件方法
addEventListener和
removeEventListener即可。顾名思义,它们分别用于添加事件和移除事件,主要用到
list和
dict来完成事件存储。代码如下:
def addEventListener(self, eventType, listener): self.mouseList.append({ "eventType" : eventType, "listener" : listener }) def removeEventListener(self, eventType, listener): for o in self.mouseList: if o["eventType"] == eventType and o["listener"] == listener: self.mouseList.remove(o) break
最后加入
MouseEvent类:
class MouseEvent(object): MOUSE_DOWN = "mouse_down" MOUSE_UP = "mouse_up" MOUSE_MOVE = "mouse_move" MOUSE_OVER = "mouse_over" MOUSE_OUT = "mouse_out" DOUBLE_CLICK = "mouse_dbclick" def __init__(): raise Exception("MouseEvent cannot be instantiated.")
使用时,这么写就可以了:
def main(): layer = Sprite() layer.scaleX = 3 addChild(layer) txt = TextField() txt.text = "Test" txt.textColor = "red" txt.x = 50 txt.y = 100 txt.size = 50 layer.addChild(txt) # mouse down event layer.addEventListener(MouseEvent.MOUSE_DOWN, onMouseDown) # mouse up event layer.addEventListener(MouseEvent.MOUSE_UP, onMouseUp) # mouse move event layer.addEventListener(MouseEvent.MOUSE_MOVE, onMouseMove) def onMouseDown(e): print("mouse down", e.offsetX, e.offsetY) def onMouseUp(e): print("mouse up", e.selfX, e.selfY) def onMouseMove(e): print("mouse move", e.target, e.currentTarget) init(30, "Sprite and Mouse Event", 800, 600, main)
运行截图如下:
至此,我们就把
Sprite和鼠标事件大致实现了。
预告:下一篇我们实现动画类。
欢迎大家继续关注我的博客
转载请注明出处:Yorhom’s Game Box
http://blog.csdn.net/yorhomwang
相关文章推荐
- Python之set集合
- python小白-day5 time&datetime模块
- python-----读写操作
- Python+Django+SAE系列教程15-----输出非HTML内容(图片/PDF)
- Python学习二
- python3 随机数
- Python实现PLA(感知机)
- Python实现LR(逻辑回归)
- Python3.4爬虫编程
- Python实现CART(基尼指数)
- Python实现C4.5(信息增益率)
- python模块paramiko与ssh
- Python实现ID3(信息增益)
- python的filter()函数
- python的reduce()函数
- python的map()函数
- python 笔记 加密模块
- python 笔记 加密模块
- Python实现nb(朴素贝叶斯)
- python re的findall和finditer