本次来实现Sprite类和鼠标事件。

说起这个Sprite啊,涉及过2D游戏研究领域的看官应该都听说过它。

它中文原意是“精灵”,只是在不同人的眼中,它所表示的意义不同。

比方说在cocos2d中。它能够是一张图片。只是在flash中,Sprite是一个相似于“层”的家伙。

当然你把它定义为层并非非常准确,实际上它是一个含显示列表的显示对象。

什么意思呢?各位看官假设阅读了前面的章节,那对显示列表并不陌生。它说白了就是一个包括其它显示对象的容器。

那或许你会想,为什么要有这个类呢?举个样例大家就明确了。在一款RPG游戏中(如:口袋妖怪)。我们的地图上有树林、小河等一系列地图元件。玩过此类游戏的同学都知道,假设我们的人物走到了地图中央继续前进的话,地图会进行卷轴移动。显示出下部分地图。

这个时候我们假设要把每一个地图元件进行移动,操作起来会相当麻烦。因此flash为我们提供的Sprite就是为了统一处理一系列显示对象而生的。

经过上面的介绍,大家可能仍然无法理解这么抽象的一个类。

那姑且把它视作一个层吧。我们能够通过SpriteaddChild来向这个层加入显示对象。加入进去的对象所进行的操作都是相对的,比方说移动,旋转。

下面是前面章节文件夹:

Python游戏引擎开发(一):序

Python游戏引擎开发(二):创建窗体以及重绘界面

Python游戏引擎开发(三):显示图片

Python游戏引擎开发(四):TextField文本类

Sprite的实现

下面是实现代码:

  1. class Sprite(DisplayObject):
  2. def __init__(self):
  3. super(Sprite, self).__init__()
  4. self.childList = []
  5. self.mouseList = []
  6. def addChild(self, child):
  7. self.childList.append(child)
  8. def removeChild(self, child):
  9. self.childList.remove(child)
  10. child.parent = None
  11. def _loopDraw(self, c):
  12. stage._showDisplayList(self.childList)

能够看到,这个类的实现代码非常easy,就是加入了显示列表属性(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类:

  1. class CanvasWidget(QtGui.QWidget):
  2. def __init__(self):
  3. super(CanvasWidget, self).__init__()
  4. self.setMouseTracking(True)
  5. def paintEvent(self, event):
  6. stage._onShow()
  7. def mousePressEvent(self, event):
  8. self.__enterMouseEvent(event, "mouse_down")
  9. def mouseMoveEvent(self, event):
  10. self.__enterMouseEvent(event, "mouse_move")
  11. def mouseReleaseEvent(self, event):
  12. self.__enterMouseEvent(event, "mouse_up")
  13. def __enterMouseEvent(self, event, eventType):
  14. e = {"offsetX" : event.x(), "offsetY" : event.y(), "eventType" : eventType, "target" : None}
  15. stage._enterMouseEvent(e, {"x" : 0, "y" : 0, "scaleX" : 1, "scaleY" : 1})

主要是重写了QWidget中的几个事件回调(mouseReleaseEventmouseMoveEventmousePressEvent)以及加入事件进入显示对象的入口__enterMouseEvent,该函数的參数一个是Qt发来的事件对象。保存了一些事件信息,如鼠标位置,还有一个是鼠标事件类型,比方”mouse_up”,”mouse_down”。

值得注意的是,setMouseTracking方法是用于不停地触发移动事件。否则Qt默认仅仅处理一次。

我们在使用鼠标事件时,多数情况下要获取鼠标的位置。所以我们将鼠标信息也记录下来,记入变量e,这个变量将随着事件的传递。一直传下去。顺便对当中的属性进行说明:

  • offsetX,offsetY:鼠标相对于屏幕左上角的位置
  • eventType:鼠标事件类型
  • target:鼠标所点击的显示对象。初始值为None,当事件传递到须要获取此属性的时候,就会被赋值为被点击的对象

最后进入Stage._enterMouseEvent函数。将事件传递到舞台对象上。

Stage._enterMouseEvent的代码例如以下:

  1. def _enterMouseEvent(self, event, cd):
  2. childList = self.childList[:: -1]
  3. currentCd = {"x" : cd["x"], "y" : cd["y"], "scaleX" : cd["scaleX"], "scaleY" : cd["scaleY"]}
  4. for o in childList:
  5. if hasattr(o, "_enterMouseEvent") and hasattr(o._enterMouseEvent, "__call__") and o._enterMouseEvent(event, currentCd):
  6. break

在这种方法中。首先接受两个參数,一个就是鼠标信息。还有一个是坐标对象(包括x,y坐标,scaleX。scaleY拉升值)。

为什么要弄个坐标对象呢?因为我们在推断显示对象是否被点击时,须要用到坐标计算。而进行坐标计算时,须要获取对象的绝对位置,这个坐标对象就是用于计算绝对位置用的。随着事件往下级对象的传递。坐标对象会作为上级对象的坐标数据往下级对象传递。从而进行递归式坐标计算,节省效率。

在当中,我们遍历了全部底层子对象而且推断能否够进入鼠标事件向下级子对象循环,假设能够(及推断有无_enterMouseEvent方法),则进行。

除此之外。为了实现鼠标事件遮挡效果,我们特地的反着遍历显示列表,也就是说先遍历得到显示在上层的对象,调用这些对象的_enterMouseEvent方法,该方法返回值若为True则代表鼠标在该显示对象上面,通过break中断遍历。

普通情况下,有_enterMouseEvent的,大半是Sprite对象。所以我们为Sprite加入这种方法:

  1. def _enterMouseEvent(self, e, cd):
  2. if not self.visible:
  3. return
  4. currentCd = self.__getVisualCoordinate(cd, self)
  5. isOn = self._isMouseOn(e, currentCd)
  6. if isOn:
  7. for o in self.childList[::-1]:
  8. if (hasattr(o, "_enterMouseEvent") and hasattr(o._enterMouseEvent, "__call__") and o._enterMouseEvent(e, currentCd)):
  9. break
  10. self.__dispatchMouseEvent(e, currentCd)
  11. return False

和Stage中的_enterMouseEvent非常相似。

当中用了子对象的_isMouseOn方法。用于推断是否点击到该对象上。而__dispatchMouseEvent用于触发鼠标事件。__getVisualCoordinate用于得到一个显示坐标,这个好比我们看三维图形直观图。实际的大小和看到的不是一样的。因为显示对象的x,y坐标是相对父对象的,所以我们通过这种方法来实现得到看到的大小和位置,及相对于屏幕左上角的绝对位置。

__getVisualCoordinate代码:

  1. def __getVisualCoordinate(self, origin, obj):
  2. return {
  3. "x" : origin["x"] + obj.x * origin["scaleX"],
  4. "y" : origin["y"] + obj.y * origin["scaleY"],
  5. "scaleX" : origin["scaleX"] * obj.scaleX,
  6. "scaleY" : origin["scaleY"] * obj.scaleY
  7. }

这种方法的详细功能。如图所看到的:

对Object B使用该方法,那么传入的origin參数相当于Object A的坐标信息,參数obj就是Object B。返回的对象中,x属性就是90。y属性就是70。

scaleXscaleY同理。

尽管Sprite是一个显示对象,可是可见的事实上是里面的BitmapTextField等子对象。所以我们在为这些显示对象加入 _isMouseOn方法时,要区分对待。对于Bitmap等多数显示对象。我们採用推断点击的位置是否在显示对象所处的矩形范围内(毕竟我们差点儿全部显示对象都是矩形的,临时这么简单实现一下),给DisplayObject类加入该方法:

  1. def _isMouseOn(self, e, cd):
  2. if not self.visible:
  3. return
  4. ox = e["offsetX"]
  5. oy = e["offsetY"]
  6. x = cd["x"]
  7. y = cd["y"]
  8. scaleX = cd["scaleX"]
  9. scaleY = cd["scaleY"]
  10. w = self._getOriginalWidth()
  11. h = self._getOriginalHeight()
  12. if x <= ox <= x + w * scaleX and y <= oy <= y + h * scaleY:
  13. e["target"] = self
  14. return True
  15. return False

以上代码还是非常好理解的。至于_getOriginalWidth_getOriginalHeight二厮。不知道大家还记得不,是前面提到的获取显示对象原始宽高(忽略scaleXscaleY)的方法。通过if x <= ox <= x + w * scaleX and y <= oy <= y + h * scaleY:推断点击的位置是否在显示对象内。

对于Sprite,加入不同的_isMouseOn方法:

  1. def _isMouseOn(self, e, cd):
  2. if not self.visible:
  3. return
  4. childList = self.childList[::-1]
  5. for o in childList:
  6. childCd = self.__getVisualCoordinate(cd, o)
  7. if o._isMouseOn(e, childCd):
  8. e["target"] = o
  9. return True
  10. return False

和其它显示对象不同的是,它通过遍历子对象。并调用它们的_isMouseOn来完毕判定鼠标是否盘旋在该Sprite上。当中设置了target属性,用于方便使用者获取点击对象。

还有一个_dispatchMouseEvent方法,用于触发鼠标事件:

  1. def __dispatchMouseEvent(self, e, cd):
  2. for o in self.mouseList:
  3. t = o["eventType"]
  4. l = o["listener"]
  5. if t == e["eventType"]:
  6. eve = object()
  7. eve.offsetX = e["offsetX"]
  8. eve.offsetY = e["offsetY"]
  9. eve.selfX = (e["offsetX"] - cd["x"]) / cd["scaleX"]
  10. eve.selfY = (e["offsetY"] - cd["y"]) / cd["scaleY"]
  11. eve.target = e["target"]
  12. eve.currentTarget = self
  13. l(eve)

该方法中。首先是遍历了鼠标事件列表。找到相应的事件,然后触发事件监听器(即回调函数)。注意,监听器接受一个參数。该參数是一个object。储存了鼠标相对于屏幕左上角的坐标(offsetXoffsetY)。以及相对于Sprite对象的坐标(selfXselfY),还能够通过currentTarget属性获取触发事件的Sprite对象。以及通过target属性获取点击到的对象。

最后加入加入事件方法addEventListenerremoveEventListener就可以。顾名思义,它们分别用于加入事件和移除事件,主要用到listdict来完毕事件存储。代码例如以下:

  1. def addEventListener(self, eventType, listener):
  2. self.mouseList.append({
  3. "eventType" : eventType,
  4. "listener" : listener
  5. })
  6. def removeEventListener(self, eventType, listener):
  7. for o in self.mouseList:
  8. if o["eventType"] == eventType and o["listener"] == listener:
  9. self.mouseList.remove(o)
  10. break

最后加入MouseEvent类:

  1. class MouseEvent(object):
  2. MOUSE_DOWN = "mouse_down"
  3. MOUSE_UP = "mouse_up"
  4. MOUSE_MOVE = "mouse_move"
  5. MOUSE_OVER = "mouse_over"
  6. MOUSE_OUT = "mouse_out"
  7. DOUBLE_CLICK = "mouse_dbclick"
  8. def __init__():
  9. raise Exception("MouseEvent cannot be instantiated.")

使用时。这么写就能够了:

  1. def main():
  2. layer = Sprite()
  3. layer.scaleX = 3
  4. addChild(layer)
  5. txt = TextField()
  6. txt.text = "Test"
  7. txt.textColor = "red"
  8. txt.x = 50
  9. txt.y = 100
  10. txt.size = 50
  11. layer.addChild(txt)
  12. # mouse down event
  13. layer.addEventListener(MouseEvent.MOUSE_DOWN, onMouseDown)
  14. # mouse up event
  15. layer.addEventListener(MouseEvent.MOUSE_UP, onMouseUp)
  16. # mouse move event
  17. layer.addEventListener(MouseEvent.MOUSE_MOVE, onMouseMove)
  18. def onMouseDown(e):
  19. print("mouse down", e.offsetX, e.offsetY)
  20. def onMouseUp(e):
  21. print("mouse up", e.selfX, e.selfY)
  22. def onMouseMove(e):
  23. print("mouse move", e.target, e.currentTarget)
  24. init(30, "Sprite and Mouse Event", 800, 600, main)

执行截图例如以下:

至此,我们就把Sprite和鼠标事件大致实现了。


预告:下一篇我们实现动画类。


欢迎大家继续关注我的博客

转载请注明出处:Yorhom’s Game Box

http://blog.csdn.net/yorhomwang

Python游戏引擎开发(五):Sprite精灵类和鼠标事件的更多相关文章

  1. Python游戏引擎开发(七):绘制矢量图

    今天来完毕绘制矢量图形. 没有读过前几章的同学,请先阅读前几章: Python游戏引擎开发(一):序 Python游戏引擎开发(二):创建窗体以及重绘界面 Python游戏引擎开发(三):显示图片 P ...

  2. 【Cocos2d-x游戏引擎开发笔记(25)】XML解析

    原创文章,转载请注明出处:http://blog.csdn.net/zhy_cheng/article/details/9128819 XML是一种非常重要的文件格式,由于C++对XML的支持非常完善 ...

  3. 如何制作一款HTML5 RPG游戏引擎——第五篇,人物&人物特效

    上一次,我们实现了对话类,今天就来做一个游戏中必不可少的——人物类. 当然,你完全是可以自己写一个人物类,但是为了方便起见,还是决定把人物类封装到这个引擎里. 为了使这个类更有意义,我还给人物类加了几 ...

  4. 推荐一些好用的 HTML5 & JavaScript 游戏引擎开发库

    推荐一些好用的 HTML5 & JavaScript 游戏引擎开发库 0. 引言 如果你是一个游戏开发者,并且正在寻找一个可以与 JavaScript 和 HTML5 无缝工作的游戏引擎.那么 ...

  5. 25 个超棒的 HTML5 & JavaScript 游戏引擎开发库

    就像在汽车中,引擎完成主要的工作,使汽车看起来不可思议.游戏引擎同理,游戏开发者完成细节的工作,使游戏看起来真实.吸引人眼球.游戏引擎负责其余的事情.早期,游戏开发者通常从草图做起,花费高昂,且不容易 ...

  6. 如何制作一款HTML5 RPG游戏引擎——第一篇,地图类的实现

    一,话说天下大事 前不久看到lufy的博客上,有一位朋友想要一个RPG游戏引擎,出于兴趣准备动手做一做.由于我研究lufylegend有一段时间了,对它有一定的依赖性,因此就准备将这个引擎基于lufy ...

  7. Cocos2d-x Lua中Sprite精灵类

    精灵类是Sprite,它的类图如下图所示. Sprite类图 Sprite类直接继承了Node类,具有Node基本特征.此外,我们还可以看到Sprite类的子类有:PhysicsSprite和Skin ...

  8. Cocos2d-JS中的Sprite精灵类

    精灵类是cc.Sprite,它的类图如下图所示.cc.Sprite类直接继承了cc.Node类,具有cc.Node基本特征. 创建Sprite精灵对象 创建精灵对象可以使用构造函数实现,它们接受相同的 ...

  9. 【python游戏编程之旅】第三篇---pygame事件与设备轮询

    本系列博客介绍以python+pygame库进行小游戏的开发.有写的不对之处还望各位海涵. 在上一篇博客中,我们学习了pygame中的IO.数据http://www.cnblogs.com/msxh/ ...

随机推荐

  1. node多项目同时运行,nginx端口监听转发

    在服务器端安装pm2 npm install npm2 -g --save 之后再项目目录下运行 pm2 start app.js 在查看进程,是否已经启动 pm2 list 多个项目,我们只要监听端 ...

  2. 如何有效地提升JavaScript 水平?

    lyuehh 努力学习中.. 9 人赞同 简单的介绍一下, 不一定准确, 大家一起进步... (单词大小写什么的别计较....) 学习js主要是一下几个方面, js语言本身的知识, 和浏览器相关的知识 ...

  3. 【积累】javascript tips代码段

    1.json转字符串 function json2str(o) { var arr = []; var fmt = function (s) { if (typeof s == 'object' &a ...

  4. NPAPI命休矣

    NPAPI命休矣,Firebreath命休矣,NPPluginProxy命休矣.以后该更多专注LLVM.Emscripten.Websocket和NativeClient之类的技术啦.

  5. java 教程

    1.视频教程 http://blog.csdn.net/zhangdaiscott/article/details/18220411 2.书籍教程: 3.学习课程

  6. 【PM日记】处理事务的逻辑

    首先你得时刻搞清楚在你的当下什么类型事情是最重要的,是与人交流,是推进项目,还是需要更加埋头学习知识. 每天你得有个list,可以是上一日遗留下来的部分未完成项,可以是idea收集箱中拿到的新任务,总 ...

  7. (转)SQL执行顺序

    SQL语句理解:http://blog.jobbole.com/55086/ 窗口函数/分析函数:http://blog.csdn.net/mfkpie/article/details/1636451 ...

  8. ShadowCaster 代码

    Pass { Name "ShadowCaster" Tags{"LightMode" = "ShadowCaster"} CGPROGRA ...

  9. poj 3414(简单bfs)

    题目链接:http://poj.org/problem?id=3414 思路:bfs简单应用,增对瓶A或者瓶B进行分析就可以了,一共6种状态. #include<iostream> #in ...

  10. EF提供的3中查询方式

    1. Linq to Entities using (TestEntities te = new TestEntities()) { var user = from a in te.User wher ...