wxPython和pycairo练习记录3
现在的感觉像是学武功的人,对着一本秘籍瞎练,没有师父,也没有参照,不知对错。之前写的有很多混乱的地方,比如主 fps 对对象 fps 的作用,比如 MovieClip 对象 surface 的宽高及对继承方法 GetWidth 的影响,等等没有影响现有结果的地方。唯一检验的标准是,实现出想要的效果。
这次想实现的是,在四个角生成敌方坦克,自动行驶,相互碰撞转向。
3.1 生成敌方坦克
手里只有一张素材图片,要区分敌我,最简单的办法就是改变颜色 ,更准确的词是改变色相。那用 mask 行吗?发现是不行,像字面意思是遮住一部分,所以得用 pycairo 的混合操作。
遮罩 mask
def DrawSceneObjects(self, ctx):
surface = cairo.ImageSurface.create_from_png("tank_T1_0.png")
# 填充方式 https://pycairo.readthedocs.io/en/latest/reference/patterns.html
pattern = cairo.SolidPattern(0, 1, 1, 0.8)
# 纯色填充作为遮罩
ctx.set_source_surface(surface, 100, 100)
ctx.mask(pattern)
# 图像作为遮罩
ctx.set_source(pattern)
ctx.mask_surface(surface, 200, 100)
混合操作 compositing operator
详见文档:https://pycairo.readthedocs.io/en/latest/reference/enums.html?highlight=operator#cairo.Operator
要改变色相,找到和色相 Hue 有关的操作,这里有 HSL_COLOR 、HSL_HUE、HSL_LUMINOSITY、HSL_SATURATION 。
HSL即色相、饱和度、亮度(英语:Hue, Saturation, Lightness)。
来源:https://baike.baidu.com/item/HSL/1443144?fr=aladdin
def DrawSceneObjects(self, ctx):
ctx.save() # 像汇编中的 pusha 保存现场
surface = cairo.ImageSurface.create_from_png("tank_T1_0.png")
ctx.set_source_surface(surface, 100, 100)
ctx.paint()
ctx.rectangle(100, 100, surface.get_width(), surface.get_height())
ctx.set_source_rgba(0, 1, 1, 0.8)
ctx.set_operator(cairo.Operator.HSL_HUE) # 将色相和目标图像色相混合
ctx.fill()
ctx.restore() # 恢复现场
3.2 自动行走和转向
玩家坦克可以由键盘事件驱动,那么没人控制的敌方坦克要怎么动起来?
单个维度很好理解,涉及多个维度,人脑很容易就混乱了。这里坦克的移动只需要修改它的xy坐标,绘制坦克的事我们之前已经交给图像容器和主程序。
转向则是分为,撞墙转向,遇到其他坦克转向。
def ChangeDirection(self, direction):
# 方向转换为除指定方向以外的方向,总觉得这样写不太好 加个
# TODO
directions = [wx.WXK_UP, wx.WXK_DOWN, wx.WXK_LEFT, wx.WXK_RIGHT]
directions.remove(direction)
self._direction = random.choice(directions)
def AutoWalk(self):
if self._direction == wx.WXK_UP:
self.Up()
elif self._direction == wx.WXK_DOWN:
self.Down()
elif self._direction == wx.WXK_LEFT:
self.Left()
elif self._direction == wx.WXK_RIGHT:
self.Right()
# 遇边界转向,坦克宽高明明是48,可是按48处理显示不对
# TODO
if self._x < 0:
self._x = 0
self.ChangeDirection(wx.WXK_LEFT)
elif self._x > cv.BOARD_WIDTH - 68:
self._x = cv.BOARD_WIDTH - 68
self.ChangeDirection(wx.WXK_RIGHT)
else:
self._x += self._dx * self._speed
if self._y < 0:
self._y = 0
self.ChangeDirection(wx.WXK_UP)
elif self._y > cv.BOARD_HEIGHT - 96:
self._y = cv.BOARD_HEIGHT - 96
self.ChangeDirection(wx.WXK_DOWN)
else:
self._y += self._dy * self._speed
def Update(self, times, speed):
# 由主程序刷新时更新
self.AutoWalk()
super().Update(times, speed)
3.3 完整代码
class Enemy(Tank):
def __init__(self, x, y, path="tank_T1_0.png", rect=(0, 0, 48, 48), fps=100, mixColor=(0, 1, 1, 0.8)):
self._mixColor = mixColor # 自定义混合颜色
super(Enemy, self).__init__(x, y, path, rect, fps)
# 初始随机方向
self._direction = random.choice([wx.WXK_UP, wx.WXK_DOWN, wx.WXK_LEFT, wx.WXK_RIGHT])
self._speed = 1
self._isPlaying = True
def LoadFrames(self, rect):
# 将图像载为帧前,先改变图像的色相
surface = cairo.ImageSurface.create_from_png("tank_T1_0.png")
sw = surface.get_width()
sh = surface.get_height()
# 创建只显示坦克的 surface 作为混合层
surface2 = cairo.ImageSurface(cairo.FORMAT_ARGB32, sw, sh)
ctx = cairo.Context(surface2)
ctx.set_source_rgba(*self._mixColor)
ctx.mask_surface(surface)
# 与目标图像混合
ctx = cairo.Context(surface)
ctx.set_source_surface(surface2)
ctx.set_operator(cairo.Operator.HSL_HUE) # 将色相和目标图像色相混合
ctx.paint()
self._surface = surface
super().LoadFrames(rect)
def GetDirection(self):
return self._direction
def ChangeDirection(self, direction):
# 方向转换为除指定方向以外的方向,总觉得这样写不太好 加个
# TODO
directions = [wx.WXK_UP, wx.WXK_DOWN, wx.WXK_LEFT, wx.WXK_RIGHT]
directions.remove(direction)
self._direction = random.choice(directions)
def AutoWalk(self):
if self._direction == wx.WXK_UP:
self.Up()
elif self._direction == wx.WXK_DOWN:
self.Down()
elif self._direction == wx.WXK_LEFT:
self.Left()
elif self._direction == wx.WXK_RIGHT:
self.Right()
# 遇边界转向,坦克宽高明明是48,可是按48处理显示不对
# TODO
if self._x < 0:
self._x = 0
self.ChangeDirection(wx.WXK_LEFT)
elif self._x > cv.BOARD_WIDTH - 68:
self._x = cv.BOARD_WIDTH - 68
self.ChangeDirection(wx.WXK_RIGHT)
else:
self._x += self._dx * self._speed
if self._y < 0:
self._y = 0
self.ChangeDirection(wx.WXK_UP)
elif self._y > cv.BOARD_HEIGHT - 96:
self._y = cv.BOARD_HEIGHT - 96
self.ChangeDirection(wx.WXK_DOWN)
else:
self._y += self._dy * self._speed
def Update(self, times, speed):
# 由主程序刷新时更新
self.AutoWalk()
super().Update(times, speed)
3.4 效果
多个对象碰撞检测,还得再看看,简单写了下发现有时候会撞在一起打转,这块先放着,所以这次只添加了敌军坦克自动行走加撞墙转向。
# -*- coding: utf-8 -*-
# example2.py
import random
import wx
import wx.lib.wxcairo
import cairo
import board
from display import MovieClip
class cv(board.cv):
"""
常量,用类属性避免使用全局变量
"""
# 面板尺寸
BOARD_WIDTH = 600
BOARD_HEIGHT = 400
class Tank(MovieClip):
def __init__(self, *args, **kwargs):
super(Tank, self).__init__(*args, **kwargs)
self._speed = 10 # 移动速度
self._dx = 0 # x 轴方向
self._dy = 0 # y 轴方向
def Up(self):
self._dy = -1
self._dx = 0
# 同一方向时需要需要排除,不然动画一直停留在第1帧
if self._currentScene != 0:
self.GotoAndPlay(0, 0)
def Down(self):
self._dy = 1
self._dx = 0
if self._currentScene != 1:
self.GotoAndPlay(0, 1)
def Left(self):
self._dx = -1
self._dy = 0
if self._currentScene != 2:
self.GotoAndPlay(0, 2)
def Right(self):
self._dx = 1
self._dy = 0
if self._currentScene != 3:
self.GotoAndPlay(0, 3)
class Player(Tank):
def __init__(self, x, y, path="tank_T1_0.png", rect=(0, 0, 48, 48), fps=100):
super(Player, self).__init__(x, y, path, rect, fps)
def OnKeyDown(self, e):
key = e.GetKeyCode()
if key == wx.WXK_UP:
self.Up()
if key == wx.WXK_DOWN:
self.Down()
if key == wx.WXK_LEFT:
self.Left()
if key == wx.WXK_RIGHT:
self.Right()
self._x += self._dx * self._speed
self._y += self._dy * self._speed
def OnKeyUp(self, e):
key = e.GetKeyCode()
if key == wx.WXK_UP or key == wx.WXK_DOWN:
self._dy = 0
if key == wx.WXK_LEFT or key == wx.WXK_RIGHT:
self._dx = 0
if self._dx == 0 and self._dy == 0:
self.Stop()
class Enemy(Tank):
def __init__(self, x, y, path="tank_T1_0.png", rect=(0, 0, 48, 48), fps=100, mixColor=(0, 1, 1, 0.8)):
self._mixColor = mixColor # 自定义混合颜色
super(Enemy, self).__init__(x, y, path, rect, fps)
# 初始随机方向
self._direction = random.choice([wx.WXK_UP, wx.WXK_DOWN, wx.WXK_LEFT, wx.WXK_RIGHT])
self._speed = 1
self._isPlaying = True
def LoadFrames(self, rect):
# 将图像载为帧前,先改变图像的色相
surface = cairo.ImageSurface.create_from_png("tank_T1_0.png")
sw = surface.get_width()
sh = surface.get_height()
# 创建只显示坦克的 surface 作为混合层
surface2 = cairo.ImageSurface(cairo.FORMAT_ARGB32, sw, sh)
ctx = cairo.Context(surface2)
ctx.set_source_rgba(*self._mixColor)
ctx.mask_surface(surface)
# 与目标图像混合
ctx = cairo.Context(surface)
ctx.set_source_surface(surface2)
ctx.set_operator(cairo.Operator.HSL_HUE) # 将色相和目标图像色相混合
ctx.paint()
self._surface = surface
super().LoadFrames(rect)
def GetDirection(self):
return self._direction
def ChangeDirection(self, direction):
# 方向转换为除指定方向以外的方向,总觉得这样写不太好 加个
# TODO
directions = [wx.WXK_UP, wx.WXK_DOWN, wx.WXK_LEFT, wx.WXK_RIGHT]
directions.remove(direction)
self._direction = random.choice(directions)
def AutoWalk(self):
if self._direction == wx.WXK_UP:
self.Up()
elif self._direction == wx.WXK_DOWN:
self.Down()
elif self._direction == wx.WXK_LEFT:
self.Left()
elif self._direction == wx.WXK_RIGHT:
self.Right()
# 遇边界转向,坦克宽高明明是48,可是按48处理显示不对
# TODO
if self._x < 0:
self._x = 0
self.ChangeDirection(wx.WXK_LEFT)
elif self._x > cv.BOARD_WIDTH - 68:
self._x = cv.BOARD_WIDTH - 68
self.ChangeDirection(wx.WXK_RIGHT)
else:
self._x += self._dx * self._speed
if self._y < 0:
self._y = 0
self.ChangeDirection(wx.WXK_UP)
elif self._y > cv.BOARD_HEIGHT - 96:
self._y = cv.BOARD_HEIGHT - 96
self.ChangeDirection(wx.WXK_DOWN)
else:
self._y += self._dy * self._speed
def Update(self, times, speed):
# 由主程序刷新时更新
self.AutoWalk()
super().Update(times, speed)
class Board(board.Board):
def DrawBackground(self, ctx):
super().DrawBackground(ctx)
text = "SmileBasic"
ctx.set_font_size(40)
_, _, w, h, _, _ = ctx.text_extents(text)
x = (cv.BOARD_WIDTH - w) // 2
y = (cv.BOARD_HEIGHT - h) // 2
# 文字是以首个字左下角坐标定位,而矩形是以左上角定位,y轴相差文字的高度,不需要考虑线条宽度。
# 另外PaintDC是不含标题栏的。
ctx.rectangle(x - 10, y - 10 - h, w + 20, h + 20)
ctx.set_source_rgb(1, 0, 0)
ctx.stroke()
ctx.move_to(x, y)
ctx.set_source_rgb(1, 1, 1)
ctx.show_text(text)
def InitSceneObjects(self):
super().InitSceneObjects()
self.player = Player(50, 50) # 实例化一个玩家坦克
self.sceneObjects.append(self.player)
# 坦克在四个角的坐标
coordinates = [(0, 0),
(cv.BOARD_WIDTH - 48, 0),
(0, cv.BOARD_HEIGHT - 48),
(cv.BOARD_WIDTH - 48, cv.BOARD_HEIGHT - 48)]
# 生成10个敌军坦克
for i in range(10):
coord = random.choice(coordinates)
enemy = Enemy(x=coord[0], y=coord[1], mixColor=(random.random(), random.random(), random.random(), 0.8))
self.sceneObjects.append(enemy)
def DrawSceneObjects(self, ctx):
for so in self.sceneObjects:
ctx.set_source_surface(so.GetSurface(), so.GetX(), so.GetY())
ctx.paint()
def OnKeyDown(self, e):
self.player.OnKeyDown(e)
self.Refresh()
def OnKeyUp(self, e):
self.player.OnKeyUp(e)