You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

2D等距视角实体深度排序公式失效问题求助(C#/MonoGame)

2D等距引擎深度排序失效修复方案

问题背景

基于C#/MonoGame自制2D等距引擎,使用自定义I/J坐标转换公式计算实体位置,通过distance_entity函数计算排序距离实现深度绘制。当前存在以下问题:

  • 深度排序仅对部分实体生效
  • 场景切换后排序结果异常,多次切换后仅部分恢复
  • 瓦片数组中的基础瓦片排序错误
  • 增加实体真实高度后问题未解决
  • 自定义实现viewport和camera,未使用MonoGame原生组件

核心问题分析

  1. 排序距离计算逻辑偏差
    • 当前distance_entity依赖转换后的sortx + sorty,但转换过程中叠加了不必要的camera/viewport偏移,且layer相关偏移错误使用tile宽度而非高度,导致层间排序逻辑失效。
    • 绘制时硬编码的I2/J2偏移(-3/-2)完全依赖特定场景的瓦片尺寸,场景切换后瓦片缩放或尺寸变化时,偏移值直接失效。
  2. 场景切换状态未重置
    • 场景切换时,camera坐标、viewport状态、实体的I/J缓存未清空或重置,旧场景的坐标残留干扰新场景排序。
  3. 基础瓦片未遵循统一排序规则
    • 基础瓦片直接按数组顺序绘制,未使用与实体一致的深度排序逻辑,导致瓦片与实体、瓦片之间的层级混乱。

具体修复步骤

1. 重构排序键计算逻辑

放弃依赖I/J转换的排序方式,改用实体底部中心世界坐标计算排序键(等距场景中,y值越大的物体应越晚绘制,即显示在上方):

public static float GetSortKey(Grid grid, Entity entity)
{
    float tileWidth = grid.info.tile.width * grid.view.viewport.scale;
    float tileHeight = grid.info.tile.height * grid.view.viewport.scale;
    // 计算实体底部中心的世界坐标
    float bottomWorldY = entity.data.y + entity.data.height * tileHeight;
    float centerWorldX = entity.data.x + (entity.data.width * tileWidth) / 2f;

    // 等距场景通用排序键:(X/2 + Y) 确保正确层级关系,叠加layer权重避免层间冲突
    float sortValue = (centerWorldX / 2f) + bottomWorldY;
    sortValue += entity.data.layer * 200f; // 200f需大于单个tile的排序差值,避免同层元素冲突

    return sortValue;
}

基础瓦片使用同样逻辑计算排序键:

public float GetSortKey(Grid grid)
{
    float tileWidth = grid.info.tile.width * grid.view.viewport.scale;
    float tileHeight = grid.info.tile.height * grid.view.viewport.scale;
    // 瓦片底部世界坐标
    float bottomWorldY = this.WorldY + tileHeight;
    float centerWorldX = this.WorldX + tileWidth / 2f;

    return (centerWorldX / 2f) + bottomWorldY;
}

2. 移除绘制逻辑中的硬编码偏移

删除I2/J2的硬编码偏移,直接使用世界坐标处理可见性判断与绘制:

float tileWidth = info.tile.width * view.viewport.scale;
float tileHeight = info.tile.height * view.viewport.scale;
float worldX = entity.data.x;
float worldY = entity.data.y;

for (int j = 0; j < entity.data.height; j++)
{
    for (int i = 0; i < entity.data.width; i++)
    {
        float currentTileWorldX = worldX + i * tileWidth;
        float currentTileWorldY = worldY + j * tileHeight;

        // 直接用世界坐标判断可见性
        if (view.viewport.is_visible(currentTileWorldX, currentTileWorldY))
        {
            // 屏幕坐标 = 世界坐标 - 相机偏移 + 视口原点
            float screenX = currentTileWorldX - view.camera.x + view.viewport.x;
            float screenY = currentTileWorldY - view.camera.y + view.viewport.y;

            Assets.blit(entity.info.asset, screenX, screenY,
                        entity.data.start_tile_i + i, entity.data.start_tile_j + j,
                        info.tile.width, info.tile.height, view.viewport.scale);
        }
    }
}

3. 场景切换时强制重置状态

在场景切换流程中,必须重置以下状态:

  • 将camera坐标重置为场景初始位置(如(0,0))
  • 清空所有实体与瓦片的排序键缓存,确保新场景加载时重新计算
  • 重置viewport的可见区域参数

4. 统一实体与瓦片的绘制顺序

绘制时先收集所有可见的瓦片与实体,按排序键从小到大排序后依次绘制(排序键越小,越先绘制,即显示在下方):

// 收集可见元素
List<IDrawable> drawables = new List<IDrawable>();
drawables.AddRange(visibleTiles);
drawables.AddRange(visibleEntities);

// 按排序键排序
drawables.Sort((a, b) => a.GetSortKey(grid).CompareTo(b.GetSortKey(grid)));

// 依次绘制
foreach (var drawable in drawables)
{
    drawable.Draw();
}

调试建议

  • 开启调试模式,绘制每个元素的排序键值与世界坐标,对比场景切换前后的数值变化,定位异常来源
  • 验证等距坐标转换公式的正确性:代入已知点(如世界原点),检查I/J计算结果是否符合预期
  • 检查viewport的is_visible函数逻辑,确保可见性判断未引入坐标偏移错误

内容的提问来源于stack exchange,提问作者Milsan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.27 17:34:55