kivy对pytmx的支持类库
pytmx只有支持pygame和pyglet的,没有针对kivy的支持,封装了一个类库:
import os
import glob
import logging
import xml.etree.ElementTree as ET
from kivy.app import App
from kivy.uix.button import Button
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.label import Label
from kivy.uix.widget import Widget
from kivy.graphics import Color, Rectangle, Line
from kivy.core.window import Window
from kivy.graphics.texture import Texture
from PIL import Image as PILImage
from typing import Optional, Tuple, Callable
import pytmx
from pytmx import TileFlags
from pytmx import TiledImageLayer, TiledObjectGroup, TiledTileLayer #TiledGroupLayer
from pytmx.pytmx import ColorLike, PointLike
from typing import Optional, Union, List
import random # 引入随机数模块
logger = logging.getLogger(__name__)
def load_tmx(
filename: str,
*args,
**kwargs,
) -> pytmx.TiledMap:
"""Load a TMX file, images, and return a TiledMap class
PYGAME USERS: Use me.
this utility has 'smart' tile loading. by default any tile without
transparent pixels will be loaded for quick blitting. if the tile has
transparent pixels, then it will be loaded with per-pixel alpha. this is
a per-tile, per-image check.
if a color key is specified as an argument, or in the tmx data, the
per-pixel alpha will not be used at all. if the tileset's image has colorkey
transparency set in Tiled, the util_pygam will return images that have their
transparency already set.
TL;DR:
Don't attempt to convert() or convert_alpha() the individual tiles. It is
already done for you.
Parameters:
filename: filename to load
Returns:
new pytmx.TiledMap object
"""
kwargs["image_loader"] = image_loader
return pytmx.TiledMap(filename, *args, **kwargs)
#def image_loader(filename: str, colorkey: Optional[ColorLike], **kwargs):
def image_loader(filename: str, colorkey: Optional[ColorLike], **kwargs) -> Callable:
"""
Image loader for Kivy.
Parameters:
filename: filename, including path, to load.
colorkey: colorkey for the image.
Returns:
function to load tile images.
"""
# 加载图像
image = PILImage.open(filename)
# 如果指定了颜色键,处理颜色键
if colorkey:
colorkey = tuple(int(colorkey[i:i + 2], 16) for i in (1, 3, 5))
image = image.convert("RGBA")
data = image.getdata()
# 替换颜色键为透明
new_data = []
for item in data:
if item[0:3] == colorkey:
new_data.append((0, 0, 0, 0)) # 变为透明
else:
new_data.append(item)
image.putdata(new_data)
pixelalpha = kwargs.get("pixelalpha", True)
# 载入一个小瓦片,一个瓦片会被调用一次
def load_image(rect=None, flags=None) -> PILImage.Image:
#logger.info(["rectAndFlags:", rect, bool(flags), flags])
if rect:
"""将(left, top, width, height) 转换为 (left, top, right, bottom)"""
left, top, width, height = rect
right = left + width
bottom = top + height
rect = (left, top, right, bottom)
try:
tile = image.crop(rect)
except ValueError:
logger.error("Tile bounds outside bounds of tileset image")
raise
else:
tile = image.copy()
#是否需要旋转翻转之类的调整
if flags:
tile = handle_transformation(tile, flags)
#logger.info(["rectAndFlags-size:", filename, rect, tile.size])
# 这个可有可无
tile = smart_convert(tile, colorkey, pixelalpha)
return tile
return load_image
def smart_convert(original: PILImage.Image, colorkey: Optional[ColorLike], pixelalpha: bool) -> PILImage.Image:
"""
Return a new Image with optimal pixel/data format.
Parameters:
original: tile image to inspect
colorkey: optional colorkey for the tileset image
pixelalpha: if true, prefer per-pixel alpha surfaces
Returns:
new tile image
"""
#logger.info(["处理聪明转变"])
if colorkey:
# Convert to RGBA and apply color key
original = original.convert("RGBA")
data = original.getdata()
# Create a new data list for the image
new_data = []
for item in data:
if item[:3] == colorkey: # Compare RGB
new_data.append((0, 0, 0, 0)) # Transparent
else:
new_data.append(item)
# Update the image data
original.putdata(new_data)
else:
# Check for transparent pixels
original = original.convert("RGBA")
data = original.getdata()
has_transparency = any(item[3] < 255 for item in data)
if not has_transparency:
# No transparent pixels, convert to RGB
original = original.convert("RGB")
elif pixelalpha:
# Prefer per-pixel alpha
pass # No change needed, already RGBA
else:
# If we don't handle transparency, convert to RGB
original = original.convert("RGB")
return original
def handle_transformation(tile: PILImage.Image, flags: TileFlags) -> PILImage.Image:
"""
Transform tile according to the flags and return a new one.
Parameters:
tile: tile surface to transform (Pillow Image).
flags: TileFlags object.
Returns:
new tile surface.
"""
#logger.info(["处理转化",tile, flags])
# 旋转 270 度
if flags.flipped_diagonally:
logger.info(["开始旋转角度",tile])
tile = tile.rotate(270, expand=True)
# 水平翻转或垂直翻转
if flags.flipped_horizontally:
logger.info(["开始水平翻转",tile])
tile = tile.transpose(PILImage.FLIP_LEFT_RIGHT)
if flags.flipped_vertically:
logger.info(["开始垂直翻转", tile])
tile = tile.transpose(PILImage.FLIP_TOP_BOTTOM)
return tile
"""强势转化PIL为KIVY纹理"""
def pil_image_to_texture(pil_image:PILImage) -> Texture:
"""
Convert a PIL Image to a Kivy Texture
"""
# Ensure the image is in RGBA mode
pil_image = pil_image.convert("RGBA")
# Get image dimensions
width, height = pil_image.size
# Get the image data
img_data = pil_image.tobytes()
# Create a new texture object
texture = Texture.create(size=(width, height))
# Flip the image data vertically (PIL and Kivy texture coordinates are inverted)
texture.flip_vertical()
# Load the image data into the texture
texture.blit_buffer(img_data, colorfmt='rgba', bufferfmt='ubyte')
return texture
'''
瓦片地图渲染
'''
class TiledRenderer(Widget):
def __init__(self, **kwargs):
super(TiledRenderer, self).__init__(**kwargs)
self.maps = glob.glob('data/*.tmx')
self.current_map_index = 0
self.map_label = Label(text='loadMap', size_hint=(1, 0.1))
self.add_widget(self.map_label)
self.load_map()
self.tmx = None
self.pixel_size = None
layout = BoxLayout(size_hint=(1, None), height=50)
next_button = Button(text='show next MAP →')
next_button.bind(on_press=self.next_map)
layout.add_widget(next_button)
self.add_widget(layout)
def hex_to_rgb(self, hex_color):
"""Convert hex color string to an (r, g, b) tuple."""
hex_color = hex_color.lstrip('#')
return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
def load_map(self):
"""加载当前地图"""
if not self.maps:
raise FileNotFoundError("未找到任何 TMX 文件。")
try:
current_map = self.maps[self.current_map_index]
self.tmx = load_tmx(current_map)
self.pixel_size = self.tmx.width * self.tmx.tilewidth, self.tmx.height * self.tmx.tileheight
logger.info(f"载入地图成功啦啦啦啦啦: {os.path.basename(current_map)}")
#logger.debug("layers:self.tmx::::",self.tmx)
if self.tmx:
self.map_label.text = f"currentMap: {os.path.basename(current_map)}"
#logger.debug(self.map_label.text)
self.draw_map()
logger.debug(f"渲染地图成功啦啦啦啦啦: {os.path.basename(current_map)}")
return None
except Exception as e:
#raise e
logger.error(f"加载地图时出现异常: {e}")
logger.error(f"发生错误!!!开始打印空地图啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊")
self.draw_empty_map()
'''开始画图囖'''
def draw_map(self) -> None:
"""Render our map to a Kivy canvas"""
"""绘制地图"""
self.canvas.clear()
# fill the background color of our render surface
if self.tmx.background_color:
r, g, b = self.hex_to_rgb(self.tmx.background_color)
with self.canvas:
Color(r / 255.0, g / 255.0, b / 255.0)
#Color(0.5, 0.5, 0.5, 1)
Rectangle(pos=(0, 0), size=(self.tmx.width * self.tmx.tilewidth, self.tmx.height * self.tmx.tileheight))
# else:
# with self.canvas:
# # 使用随机颜色块
# Color( random.random(), random.random(), random.random(), 1) # 随机颜色RGB
# Rectangle(pos=(0, 0), size=(500,500))
'''
# 确保绘制瓦片,即使解码失败
for y in range(10): # 假设地图高度为10
for x in range(10): # 假设地图宽度为10
tile_id = tile_ids[y * 10 + x] if y * 10 + x < len(tile_ids) else 0
color = (0.5, 0.5, 0.5, 1) if tile_id != 0 else (1, 1, 1, 1) # 空瓦片为白色
with self.canvas:
Color(*color)
Rectangle(pos=(x * tile_size, y * tile_size), size=(tile_size, tile_size))
'''
# iterate over all the visible layers, then draw them
for layer in self.tmx.visible_layers:
# each layer can be handled differently by checking their type
if isinstance(layer, TiledTileLayer):
self.render_tile_layer(layer)
elif isinstance(layer, TiledObjectGroup):
self.render_object_layer(layer)
elif isinstance(layer, TiledImageLayer):
self.render_image_layer(layer)
'''渲染瓦片'''
def render_tile_layer(self, layer:TiledTileLayer) -> None:
"""Render all TiledTiles in this layer"""
logger.debug(["渲染工作中:::render_tile_layer 渲染瓦片",TiledTileLayer])
# deref these heavily used references for speed
tw = self.tmx.tilewidth
th = self.tmx.tileheight
#logger.critical((self.tmx.tilewidth,self.tmx.tileheight))
# iterate over the tiles in the layer, and blit them
if self.tmx.orientation == "orthogonal":
logger.debug("orientation=orthogonal Orthogonal正交俯视地图")
index = 0
for x, y, pil_image in layer.tiles():
index+=1
# 计算瓦片位置
pos_x = x * tw
pos_y = Window.height - (y + 1) * th # 反转Y轴(原来的是y * th)
# 检查坐标的正确性
if x < 0 or y < 0:
raise ValueError(f"Invalid coordinates: x={x}, y={y}")
#print("ssssssssss1=",x, y, pil_image)
# Assuming `image` is already a Kivy texture
texture = pil_image_to_texture(pil_image)
#print("ssssssssss2=",type(texture))
#logger.debug([f'{index}.画一个瓦片A: pos=({x * tw}, {y * th}), size=({tw}, {th})',pil_image.size,])
#logger.debug([f'{index}.画一个瓦片AAA: pos=({pos_x}, {pos_y}), size=({tw}, {th})',pil_image.size,])
with self.canvas:
#Rectangle(texture=texture, pos=(pos_x, pos_y), size=(32, 32))
Rectangle(texture=texture, pos=(pos_x, pos_y), size=(tw, th))
elif self.tmx.orientation == "isometric":
logger.debug("orientation=isometric Isometric等距立体感地图")
# this value is used later to render the entire map to a pygame surface
ox = self.pixel_size[0] // 2
tw2 = tw // 2
th2 = th // 2
for x, y, image in layer.tiles():
sx = x * tw2 - y * tw2
sy = x * th2 + y * th2
with self.canvas:
Rectangle(texture=image, pos=(sx + ox, sy), size=(tw, th))
'''渲染对象'''
def render_object_layer(self, layer:TiledObjectGroup) -> None:
"""Render all TiledObjects contained in this layer"""
logger.debug(["渲染工作中:::render_object_layer 渲染对象",TiledObjectGroup,layer])
# these colors are used to draw vector shapes, like polygon and box shapes
rect_color = (255, 0, 0) # Kivy's Color uses 0-1 scale
poly_color = (0, 255, 0)
# iterate over all the objects in the layer
#logger.debug(["forShowLayerObj-layer=", layer])
for obj in layer:
logger.info(["forShowLayerObj", obj, obj.image,obj.x,obj.y])
tw, th = obj.image.size
# 计算瓦片位置
pos_x = obj.x + 0
pos_y = Window.height - (obj.y + 0) # 反转Y轴(跟上面的计算瓦片位置又不同)
if obj.image:
logger.debug([f'画一个瓦片BBB: pos=({pos_x}, {pos_y}), size=', obj.image.size])
texture = pil_image_to_texture(obj.image)
# Render the object with its image
with self.canvas:
Rectangle(texture=texture, pos=(pos_x, pos_y), size=obj.image.size)
elif False:
#这个是备用的
with self.canvas:
Rectangle(texture=texture, pos=(obj.x, obj.y), size=(obj.width, obj.height))
elif hasattr(obj, "points"):
# Draw the object's polygon or line
#draw_lines(poly_color, obj.closed, obj.points, 3)
logger.warning("污点点,啥也不干")
pass
else:
logger.warning("污图片,开始画单纯的线条")
points = obj.apply_transformations()
with self.canvas:
Color(*rect_color)
Line(points=points, width=2)
''' 渲染图像'''
def render_image_layer(self, layer:TiledImageLayer) -> None:
logger.debug(["渲染工作中:::render_image_layer 渲染图片",TiledImageLayer])
if layer.image:
# Assuming `layer.image` is a Kivy texture
with self.canvas:
Rectangle(texture=layer.image, pos=(0, 0), size=(self.tmx.width * self.tmx.tilewidth, self.tmx.height * self.tmx.tileheight))
'''画个空图'''
def draw_empty_map(self):
"""绘制一个空白的地图,显示为灰色色块"""
self.canvas.clear()
tile_size = 32
for y in range(10):
for x in range(10):
with self.canvas:
# 使用随机颜色块
Color( random.random(), random.random(), random.random(), 1) # 随机颜色RGB
Rectangle(pos=(x * tile_size, y * tile_size), size=(tile_size, tile_size))
'''下一个地图'''
def next_map(self, instance):
self.current_map_index = (self.current_map_index + 1) % len(self.maps)
logger.debug([self.maps, self.maps[self.current_map_index]])
self.load_map()
class MyApp(App):
def build(self):
return TiledRenderer()
if __name__ == '__main__':
#logging.basicConfig(level=logging.INFO)
#解压pytmx的base64数据
'''
pybase64 = 'eJyNkMEKADAIQh32/9+8HQqk2djhEWWYRABxYBJSla5Pfc1W0n1U+8H5l6a9y07MOZx/3+1ZaHyJO6+79fqVzjfbTAEP'
logger.critical(pytmx.pytmx.unpack_gids(pybase64,'base64','zlib'))
# [CRITICAL] [3, 4, 3, 3, 4, 3, 4, 4, 4, 4, 3, 3, 4, 3, 3, 3, 4, 3, 3, 3, 3, 4, 1, 1, 4, 4, 4, 4, 4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4, 3, 3, 3, 3, 1, 1, 3, 3, 1, 3, 4, 3, 4, 4, 3, 1, 1, 4, 4, 4, 4, 1, 1, 4, 3, 3, 1, 1, 4, 4, 1, 1, 1, 1, 3, 4, 3, 1, 1, 3, 1, 1, 1, 4, 4, 4, 4, 4, 1, 3, 4, 3, 3, 3, 3, 4, 4, 3, 3]
'''
Window.size = (600,600)
MyApp().run()