wxPython和pycairo练习记录17

Animation.py 实现动画刷新逻辑,Sprite.py 绘制精灵图到窗口,直接照搬代码,基本不用改动,然后就可以用来加载精灵图了 https://github.com/mx0c/super-mario-python/blob/master/classes/Sprites.py

不要当过程完美主义者,很容易自我攻击。Sprites.py 的代码看着一大堆,其实就是读取精灵图的 json 配置文件,按配置截取图像并分别转换为 Sprite 对象,Sprite 和 Animation 职责有些混乱。这些数据的主要区别是参数名称不统一,动画的图像数据是多个图像组成的列表,所以映射统一名字了也要分静态图和动画两个部分,还是直接照抄吧。

测试的时候发现,读取 gif 格式图片会报错,每种图片数据格式都不一样,需要不同的处理程序,cairo 中除了 png 并没有相应的处理程序。同时还有一个问题,之前用 cairo 实现对图片缩放,明显变模糊了。按理说素材图片虽然是位图但因为是纯色,手动缩放显示都没问题,不知道为什么会这样,网上解释说是"Cairo 默认使用线性插值算法进行缩放操作,而线性插值会导致图像像素的颜色被平滑地混合,从而造成图像的模糊效果。"。

当然,cairo 的优势是绘制图形,还是先用 wxPython 处理吧。直接用 wx.Image 加载所有格式图片,调用 Scale 方法进行缩放,GetSubImage 方法截取图像,最后再转换成 cairo.ImageSurface(纯粹为了用 cairo)。

wx.Image 默认是 RGBA 字节集,主要用于加载和处理图像。wx.Bitmap 默认为可选 alpha 通道的 RGB 字节集,主要用于绘制和显示图像。详细介绍见 https://docs.wxpython.org/wx.Image.html
wx.Image 加载不透明图片,也会默认创建值为 255 的 alpha 通道。素材中的 characters.gif,用 wx.Image 加载后,调用 GetAlpha 方法发现得到不正确的透明通道值,而且 HasAlpha 得到的结果是 False,因为没有正确解释透明通道数据,RGB 数据也错了,加载后整个图片都变成了粉色。

import wx
app = wx.App()
image = wx.Image("./img/characters.gif")
print("---characters.gif---")
print("RGB data: ", tuple(image.GetData())[:20])
print("Alpha data", tuple(image.GetAlpha())[:20])
print(image.HasAlpha())

image2 = wx.Image("./img/tiles.png")
print("---tiles.png---")
print("RGB data: ", tuple(image2.GetData())[:20])
print("Alpha data", tuple(image2.GetAlpha())[:20])
print(image2.HasAlpha())

"""
---characters.gif---
RGB data:  (255, 0, 255, 255, 0, 255, 255, 0, 255, 255, 0, 255, 255, 0, 255, 255, 0, 255, 255, 0)
Alpha data (192, 0, 50, 1, 168, 166, 98, 13, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6)
False
---tiles.png---
RGB data:  (156, 74, 0, 255, 206, 198, 255, 206, 198, 255, 206, 198, 255, 206, 198, 255, 206, 198, 255, 206)
Alpha data (255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255)
True
"""

如果有兴趣可以自己尝试写一个能够正确解析数据的处理器 https://docs.wxpython.org/wx.ImageHandler.html ,这里还是直接用工具将 gif 转为 png 图片。

还有一些需要注意的点,比如 python 中 int 是向下取整,会直接舍弃小数,为了准确,在缩放尺寸时用 round 取近似值。

至此,每个精灵图都有了自己的名字,有点像物理魔法使马修给肌肉取名,莫名的喜感,调用的时候更直观。这个单元测试不知道该怎么写,简单测试输出所有处理后的 Sprite 图像,稍微调整一些坐标和尺寸,其中 koopa-hiding-with-legs 坐标完全不对,根据名字也不知道是哪个,后续再改。

from mario.classes.Sprites import Sprites
sprites = Sprites()
for name, sprite in sprites.sprite_collection.items():
    if sprite.animation is not None:
        # 动画默认显示第一帧
        surface = sprite.animation.image
    else:
        surface = sprite.image
    surface.write_to_png(f"./test/{name}.png")

Sprites

完整代码:

# Animation.py
class Animation:
    def __init__(self, images, idle_sprite=None, air_sprite=None, delta_time=7):
        self.images = images  # 动画逐帧图像 Surface 列表
        self.timer = 0  # 主程序刷新次数计次
        self.index = 0  # 当前动画帧索引
        self.image = self.images[self.index]  # 当前动画帧图像,默认显示第一帧
        self.idle_sprite = idle_sprite  # 空闲状态精灵图,奇怪的设定
        self.air_sprite = air_sprite  # 在空中状态精灵图
        self.delta_time = delta_time  # 动画图像更新间隔帧数

    def update(self):
        # 原程序设置了 FPS(Frames Per Second) 为 60 帧/秒,每 7 帧更新一帧动画图像,即 (1000 毫秒 / 60) * 7 约每 0.12 秒
        # 人的大脑视觉处理中心,大约会将看到的画面在视觉里暂留100至400毫秒,因此,当眼前的画面每秒变换十五次以上的时候,人的大脑就会有了“这是连续动作”的错觉。
        self.timer += 1
        if self.timer % self.delta_time == 0:
            # 头尾循环取动画图像
            if self.index < len(self.images) - 1:
                self.index += 1
            else:
                self.index = 0
            self.image = self.images[self.index]

    def idle(self):
        self.image = self.idle_sprite

    def in_air(self):
        self.image = self.air_sprite

# Sprite.py
from cairo import Context


class Sprite:
    def __init__(self, image, colliding, animation=None, redraw_background=False):
        self.image = image  # 调用 SpriteSheet.image_at 返回的 Surface
        self.colliding = colliding  # 碰撞,并未发现使用
        self.animation = animation  # Animation 实例,与 Sprite 构成关联关系(依赖像租房,关联像买房)
        self.redraw_background = redraw_background

    def draw_sprite(self, x, y, screen):
        # 背景 tile 尺寸 16*16,sprite json 配置文件中将每个精灵图都放大了 2 倍
        dimensions = (x * 32, y * 32)
        # screen 是表示窗口或屏幕的 Surface 对象。screen = pygame.display.set_mode(windowSize)
        # 因为牵涉到 screen 的定义,wxPython 和 pygame 是不同的,这里假定它为 Surface 对象,后续再修改
        ctx = Context(screen)
        if self.animation is None:
            surface = self.image
        else:
            self.animation.update()
            surface = self.animation.image
        # 将源 surface 绘制到窗口上
        ctx.set_source_surface(surface, *dimensions)
        ctx.paint()

# Sprites.py
import json
from mario.classes.Animation import Animation
from mario.classes.Sprite import Sprite
from mario.classes.SpriteSheet import SpriteSheet


class Sprites:
    def __init__(self):
        self.sprite_collection = self.load_sprites(
            [
                "./sprites/Mario.json",
                "./sprites/Goomba.json",
                "./sprites/Koopa.json",
                "./sprites/Animations.json",
                "./sprites/BackgroundSprites.json",
                "./sprites/ItemAnimations.json",
                "./sprites/RedMushroom.json"
            ]
        )

    @staticmethod
    def load_sprites(file_list):
        res_dict = {}
        for file in file_list:
            with open(file, "r") as json_data:
                data = json.load(json_data)
            ss = SpriteSheet(data["spriteSheetURL"])
            dic = {}
            if data["type"] == "background":
                for sprite in data["sprites"]:
                    dic[sprite["name"]] = Sprite(
                        ss.image_at(
                            sprite["x"],
                            sprite["y"],
                            sprite["scalefactor"],
                            sprite.get("colorKey", None)
                        ),
                        sprite["collision"],
                        None,
                        sprite["redrawBg"]
                    )
                res_dict.update(dic)
            elif data["type"] == "animation":
                for sprite in data["sprites"]:
                    images = []
                    for image in sprite["images"]:
                        images.append(
                            ss.image_at(
                                image["x"],
                                image["y"],
                                image["scale"],
                                sprite["colorKey"]
                            )
                        )
                    dic[sprite["name"]] = Sprite(
                        None,
                        None,
                        Animation(images, delta_time=sprite["deltaTime"])
                    )
                res_dict.update(dic)
            elif data["type"] == "character" or data["type"] == "item":
                for sprite in data["sprites"]:
                    xsize, ysize = sprite.get("xsize") and (sprite["xsize"], sprite["ysize"]) or data.get("size")
                    dic[sprite["name"]] = Sprite(
                        ss.image_at(
                            sprite["x"],
                            sprite["y"],
                            sprite["scalefactor"],
                            sprite.get("colorKey", None),
                            False,
                            xsize,
                            ysize
                        ),
                        sprite["collision"]
                    )
                res_dict.update(dic)
        return res_dict

# SpriteSheet.py
import wx
from wx.lib.wxcairo import ImageSurfaceFromBitmap
from mario.classes.Utils import Utils


class SpriteSheet:
    def __init__(self, filename):
        try:
            # 针对提示 ICCP:known incorrect sRGB profile 图像文件中的sRGB ICC配置文件信息被识别为不正确或已经损坏
            # wx.Log.EnableLogging(enable=False)
            wx.Image.SetDefaultLoadFlags(0)
            # cairo 不支持直接导入其他格式图片,这里通过 wxPython 处理
            self.sprite_sheet = wx.Image(filename)
        except Exception as e:
            print(str(e))
            raise SystemExit

    def image_at(self, x, y, scale=1, color_key=None, is_column_row=True, tile_width=16, tile_height=16):
        if is_column_row:
            # x, y 作为行和列,取精灵图范围
            rect = wx.Rect(x * tile_width, y * tile_height, tile_width, tile_height)
        else:
            # x, y 作为坐标,取精灵图范围
            rect = wx.Rect(x, y, tile_width, tile_height)

        # 从精灵序列图中提取指定区域并创建为新的 wx.Image
        image = self.sprite_sheet.GetSubImage(rect)

        # 缩放到指定尺寸
        if scale != 1:
            image = image.Scale(round(image.GetWidth() * scale), round(image.GetHeight() * scale))

        # 将处理过的 wx.Image 对象转换为 cairo.ImageSurface 对象
        surface = ImageSurfaceFromBitmap(image.ConvertToBitmap())

        # 处理透明色
        if color_key is not None:
            if color_key == -1:
                # 取 surface 像素数据
                data = surface.get_data()
                # 取 surface 第一个点 rgb 像素值
                color_key = tuple(data[:3])
            else:
                # surface data 实际排列为 bgra
                color_key = tuple(color_key[::-1])
            # 将 color_key 设为透明
            surface = Utils.set_colorkey(surface, color_key)

        return surface

  • 23
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值