wxPython和pycairo练习记录9
9.1 键盘事件处理
想要实现的效果是,同时按下多个方向,无论松开或按下其他键,总是响应剩余最近按下的键,这里直接用列表存储处在按下状态的按键代码。
测试 wxPython 按下多个方向后松开最近按下的,之前保持按住的会失效 。那就不能靠按键直接改变坐标,只改变速度方向,然后坐标的计算移动交给定时器刷新。射击按键行为也交给定时器,只改变状态,不执行操作。
效果看起来还不错。之后可以看到,坦克和砖墙碰撞时的回弹问题也没有了。那么之前不是因为 wxPython 控件刷新用 Update 还是 Refresh 导致的,而是键盘事件处理逻辑没弄对。
class Player(Tank):
def __init__(self, x, y, surface=cv.surfaces["player"], rect=(0, 0, 48, 48), fps=100):
super(Player, self).__init__(x, y, surface, rect, fps)
self._direction = wx.WXK_UP # 当前方向
self._keydown = [] # 当前按下的方向键
def OnKeyDown(self, e):
key = e.GetKeyCode()
# 方向控制
if self._directions.get(key):
self._dx = 0
self._dy = 0
self._direction = key
self._directions[key]()
if key not in self._keydown:
self._keydown.append(key)
# 开火
if key == wx.WXK_SPACE and self._weapon:
self._weapon.SetFireState(True)
def OnKeyUp(self, e):
key = e.GetKeyCode()
if self._directions.get(key):
# 按键弹起,从按下方向列表移除
self._keydown.remove(key)
# 重置速度方向
self._dx = 0
self._dy = 0
if self._keydown:
# 方向设为最后按键方向
self._direction = self._keydown[-1]
self._directions[self._direction]()
else:
# 按键全部弹起后,移动和动画停止
self.Stop() # 停止动画
# 停火
if key == wx.WXK_SPACE and self._weapon:
self._weapon.SetFireState(False)
def GetDirection(self):
return self._direction
def UpdateXY(self):
# 坐标移动
self._x += self._dx * self._speed
self._y += self._dy * self._speed
def Update(self):
# 交给定时器执行
if self._dx or self._dy:
self.UpdateXY()
if self._weapon.IsFire():
self._weapon.Fire()
super().Update()
9.2 雷达
雷达主要目的是检测范围,有两个作用,一个感应进入范围,一个是感应离开范围。范围为障碍外边框到雷达最外层之间的区域,多层范围则为相应范围外边框到最外层之间的区域,相当于取两个矩形的差集。
碉堡是按多层感应,需要写好相应的雷达和各层范围处理函数。泥坑使用默认10像素距离的雷达检测离开行为。当然,暂时还是没解决相邻障碍作用相互影响的问题。
class Blockhouse(Obstacle):
# 碉堡
def __init__(self, x, y, surface=cv.surfaces["wall_blockhouse"], parentLayer=cv.layer, radius=150):
super(Blockhouse, self).__init__(x, y, surface, parentLayer)
self._canCross = False # 不可穿行
self._canDestroy = True # 可打破
self._canBulletCross = False # 子弹不可穿行
self._buff = False # 无特殊效果
self._radius = radius # 雷达半径
self._direction = wx.WXK_UP # 射击方向
# 加载武器和子弹
self._weapon = Weapon(capacity=350, speed=1, interval=500)
self._weapon.Loaded(self)
self._weapon.SetFireState(True)
for i in range(350):
self._weapon.AddBullet(Bullet(type=3))
# 绑定雷达
self._radar = BlockhouseRadar(self, self._radius)
def LoadWeapon(self, weapon):
weapon.Loaded(self)
self._weapon = weapon
def GetWeapon(self):
return self._weapon
def GetDirection(self):
return self._direction
def GetRadar(self):
return self._radar
def Destroy(self):
self._destroyed = True
self._weapon.Destroy()
self._radar.Destroy()
def Fire(self, tank, interval):
# 坦克进入范围射击,并调整相应开火间隔时间
self._weapon.SetInterval(interval)
# 向坦克调整射击方向并射击
self._direction = self._radar.DetectDirection(tank)
# 正方向即开火
if self._direction:
self._weapon.Fire()
def Act(self, tank):
self.Fire(tank, 100)
def SecondAct(self, tank):
self.Fire(tank, 500)
def ThirdAct(self, tank):
self.Fire(tank, 1000)
class Mud(Obstacle):
# 泥坑
def __init__(self, x, y, surface=cv.surfaces["wall_mud"], parentLayer=cv.layer1):
super(Mud, self).__init__(x, y, surface, parentLayer)
self._canCross = True # 可穿行
self._canDestroy = False # 不可打破
self._canBulletCross = True # 子弹可穿行
self._buff = True # 有特殊效果
# 绑定雷达,默认半径,用于检测离开行为
self._radar = Radar(self)
def GetRadar(self):
return self._radar
def Buff(self, tank):
# 进入范围
if tank.GetSpeed() > 1:
tank.SetSpeed(1)
def Act(self, tank):
# 离开范围,经过默认雷达范围
if tank.GetSpeed() == 1:
tank.ResetSpeed()
# -*- coding: utf-8 -*-
# radar.py
import wx
import wx.lib.wxcairo
import cairo
from const import cv
from layer import Layer
class Radar:
def __init__(self, sprite, radius=10, parentLayer=cv.layer4):
self._sprite = sprite # 绑定对象
self._radius = radius # 作用半径
self.SetRadius(self._radius)
self._destroyed = False # 销毁状态
# 默认加入默认图层
self._layer = Layer(self)
parentLayer.Append(self._layer)
def GetX(self):
return self._x
def GetY(self):
return self._y
def GetRect(self):
return wx.Rect(self._x, self._y, self._width, self._height)
def GetRadius(self):
return self._radius
def SetRadius(self, radius):
x, y, width, height = self._sprite.GetRect()
self._width = width + radius * 2
self._height = height + radius * 2
self._x = x - radius
self._y = y - radius
def GetSprite(self):
return self._sprite
def Bind(self, sprite):
self._sprite = sprite
self.SetRadius(self._radius)
def GetLayer(self):
return self._layer
def SetParentLayer(self, layer):
self._layer.SetParent(layer)
def IsDestroyed(self):
return self._destroyed
def Destroy(self):
self._destroyed = True
def DetectDirection(self, obj):
# 识别目标是否在绑定对象正对方向
tx, ty, tw, th = obj.GetRect()
x, y, w, h = self._sprite.GetRect()
direction = None # 目标所在正方向
if tx > x - tw and tx < x + w:
if ty < y:
# print("up")
direction = wx.WXK_UP
else:
# print("down")
direction = wx.WXK_DOWN
if ty > y - th and ty < y + h:
if tx < x:
# print("left")
direction = wx.WXK_LEFT
else:
# print("right")
direction = wx.WXK_RIGHT
return direction
def InRange(self, obj, distance):
# 判断目标是否在范围内,范围取指定范围到最外层之间的区域
sx, sy, sw, sh = self._sprite.GetRect()
ox, oy, ow, oh = obj.GetRect()
left = sx - distance - ow
right = sx + sw + distance
up = sy - distance - oh
down = sy + sh + distance
if ox < left or ox > right or oy < up or oy > down:
return True
else:
return False
def CheckOptActions(self, obj):
# 绑定雷达的对象默认需要实现可执行方法 Act,响应离开行为
if self.InRange(obj, 0):
return self._sprite.Act
class BlockhouseRadar(Radar):
def CheckOptActions(self, obj):
if self.InRange(obj, 50):
action = self._sprite.ThirdAct
elif self.InRange(obj, 30):
action = self._sprite.SecondAct
else:
action = self._sprite.Act
return action
9.3 单帧动画
得益于之前将 Sprite 的数据和绘制的更新都托管给 Layer ,创建基于单独一个 surface 的动画变得非常容易。
比如,创建一个用于装饰头像或物品的光线流动的线框,只需要四步:继承 Sprite ,设置绘制时需要更新的变量,绘制图像,编写变量更新规则。
pycairo 有专门用于设置填充模式的 Pattern ,比如之前用过的 cairo.SolidPattern 是纯色填充,另外还有线性渐变、径向渐变,有点像代码版的 photoshop 。
pycairo 绘图,找一个相似的概念应该叫矢量蒙版,设置一个底图 source ,上面可以绘制路径 path ,然后执行 stroke 、fill 或 paint 操作就得到结果图像 surface 。
class Box(Sprite):
def __init__(self, x, y, width, height, parentLayer=cv.layer):
self.angle = 0
self._width = width
self._height = height
surface = self.Draw()
super(Box, self).__init__(x, y, surface, parentLayer=parentLayer)
def Draw(self):
# 主图像,颜色需要 alpha 实现只显示绘制内容
width = self._width
height = self._height
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height)
ctx = cairo.Context(surface)
# 设置径向渐变模式
pattern = cairo.RadialGradient(width / 2, height / 2, 0, width / 2, height / 2, max(width, height))
# 设置颜色渐变的起始点,有点像 CSS 的 keyframes
pattern.add_color_stop_rgb(0, 0, 0.5, 1)
pattern.add_color_stop_rgb(0.5, 1, 1, 0)
pattern.add_color_stop_rgb(1, 1, 0, 0)
ctx.set_source(pattern)
# 绘制矩形边框路径
ctx.rectangle(0, 0, width, height)
ctx.set_line_width(3)
# 绘图,相当于确定需要绘制的部分
ctx.stroke()
# 绘制黄色矩形线框
surface2 = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height)
ctx2 = cairo.Context(surface2)
ctx2.rectangle(0, 0, width, height)
ctx2.set_line_width(3)
ctx2.set_source_rgb(1, 1, 0)
ctx2.stroke()
# 绘制夹角45度扇形,角度变量瞬时针旋转
surface3 = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height)
ctx3 = cairo.Context(surface3)
# 移动到圆心,并以圆心连线弧形得到扇形
ctx3.move_to(width / 2, height / 2)
# 每次旋转5度
ctx3.arc(width / 2, height / 2, max(width, height), math.pi / 36 * self.angle, math.pi / 36 * self.angle + math.pi / 4)
ctx3.close_path()
ctx3.fill()
# 设置混合模式,取黄色线框与扇形相交处绘制
ctx2.set_source_surface(surface3)
# DEST_IN ,取交集,dest 为 surface3,in 表示显示在里面
ctx2.set_operator(cairo.Operator.DEST_IN)
ctx2.paint()
# 把混合后的线框绘制到主图渐变边框上,后绘制显示在上面
ctx.set_source_surface(surface2)
ctx.paint()
return surface
def Update(self):
# 更新变量,默认 Layer 更新数据时调用
# 每次增加5度,最多360度
self.angle += 1
self.angle = self.angle % 72
self._surface = self.Draw()