Pygame开发2D Minecraft的Tile based lighting实现方案
Pygame 2D类Minecraft瓦片光照实现方案
你的现有Tile类已经预留了0-1范围的light_level字段,不用大改核心结构就能跑通瓦片级光照,比常规逐像素叠层方案性能高很多,还能实现不透明方块挡光的经典MC光照效果。
核心逻辑
直接用BFS广度优先光线传播+绘制叠暗层的方案,完全适配tile based架构:
- 先给所有方块配置透光率参数,空气完全透光,石头、基岩这类固体方块完全不透光,玻璃、树叶这类方块可以设半透值
- 光照传播只在方块变更、光源(火把、天空光)变化时计算,不用每帧全图重算,哪怕千格规模的地图也不会有性能压力
- 绘制时根据每个方块的亮度值,在原贴图上叠对应透明度的黑色遮罩,亮度1时遮罩全透明,亮度0时遮罩纯黑,不用改你现有贴图加载、世界生成的逻辑
代码修改步骤
1. 新增方块透光率配置
把配置加在World类的初始化方法里,和你现有的block_textures配置放在同层级即可:
# 世界类初始化时添加,值为方块透光率,1为完全不挡光,0为完全挡光 self.block_opacity = { "terrain": { "air": 1.0, "grass_block": 0.0, "dirt": 0.0, "stone": 0.0, "bedrock": 0.0 # 后续新增半透方块比如玻璃、树叶,直接在这里加对应透光率即可 }, "missing": { "missing_texture": 0.0 } }
2. 给World类添加光照计算方法
直接把下面的方法复制到World类里,支持人工光源+天空光自动计算,自动处理方块挡光逻辑:
def calc_lighting(self, light_sources, decay_per_tile=0.07): """ 计算全图光照 :param light_sources: 人工光源列表,格式为(格子x坐标, 格子y坐标, 初始亮度0-1),比如火把可以设为(x,y,0.9) :param decay_per_tile: 每传播一格的亮度衰减值,数值越小光照传播距离越远 """ from collections import deque # 重置所有方块亮度 for x in range(self.width): for y in range(self.height): self.tiles[x][y].light_level = 0 propagation_queue = deque() # 所有人工光源入队 for sx, sy, init_brightness in light_sources: if 0 <= sx < self.width and 0 <= sy < self.height: self.tiles[sx][sy].light_level = init_brightness propagation_queue.append((sx, sy, init_brightness)) # 四方向BFS传播,要更柔和的斜向传播可以加(1,1)(1,-1)等四个斜向方向 directions = [(1,0), (-1,0), (0,1), (0,-1)] while propagation_queue: x, y, current_bright = propagation_queue.popleft() next_bright = current_bright - decay_per_tile if next_bright <= 0: continue for dx, dy in directions: nx, ny = x+dx, y+dy if not (0 <= nx < self.width and 0 <= ny < self.height): continue target_tile = self.tiles[nx][ny] # 获取目标方块的透光率 block_trans = self.block_opacity.get(target_tile.block_category, {}).get(target_tile.block_type, 0.0) spread_bright = next_bright * block_trans # 只有新亮度高于方块现有亮度才更新,避免重复计算 if spread_bright > target_tile.light_level: target_tile.light_level = spread_bright propagation_queue.append((nx, ny, spread_bright)) # 叠加天空光:从地表往下传播,碰到不透明方块就阻断 for x in range(self.width): sky_bright = 1.0 for y in range(self.height): current_tile = self.tiles[x][y] block_trans = self.block_opacity.get(current_tile.block_category, {}).get(current_tile.block_type, 0.0) # 碰到完全不透明方块,下方天空光直接切断 if block_trans == 0: sky_bright = 0 current_tile.light_level = min(1.0, current_tile.light_level + sky_bright) # 天空光随深度衰减 sky_bright = sky_bright * block_trans - decay_per_tile if sky_bright < 0: sky_bright = 0
3. 修改Tile类的draw方法
在原有贴图绘制逻辑后加暗层叠绘即可:
def draw(self, cam_x, cam_y): # 保留你原有的绘制逻辑,比如计算屏幕坐标、blit原贴图 screen_x = self.x - cam_x screen_y = self.y - cam_y self.win.blit(self.image, (screen_x, screen_y)) # 叠加光照暗层 if self.light_level < 1.0: dark_mask = pygame.Surface((self.width, self.height)).convert_alpha() mask_alpha = int(255 * (1 - self.light_level)) dark_mask.fill((0, 0, 0, mask_alpha)) self.win.blit(dark_mask, (screen_x, screen_y))
性能优化技巧
- 不用每帧调用
calc_lighting,只在玩家放置/破坏方块、手持火把移动位置时,重算受影响范围的光照即可,200*200规模的地图就算全图重算也不会卡 - 可以提前预生成0-255透明度的黑色遮罩Surface缓存起来,绘制时直接取对应透明度的遮罩用,省掉每帧新建Surface的开销
- 想要更柔和的光照边缘,可以把传播方向改成八方向,或者最后给全图光照值做一次简单的邻域均值模糊
- 做昼夜循环只需要调整天空光的初始亮度就行,比如晚上把天空光初始值设为0.2,就能实现夜晚变暗的效果
内容的提问来源于stack exchange,提问作者coder
相关产品推荐
相关产品推荐

