wxPython和pycairo练习记录9

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()

猜你喜欢

转载自blog.csdn.net/SmileBasic/article/details/126538346