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

Monogame 2D瓦片平台游戏帧率下降问题及代码优化求助

问题描述

我用MonoGame开发2D瓦片平台游戏,全屏模式下显示较多瓦片时帧率下降卡顿。通过FrameCounter确认卡顿源于UpdateTiles方法,若强制每次更新都重建瓦片列表(把if (viewArea != oldViewArea)改成if (true)),帧率会暴跌至2帧左右。

当前实现细节:

  • 每个瓦片对应独立Tile类,存储动画、位置、类型等信息
  • Level类维护瓦片列表,相机视口变化时清空列表并重新创建视口内的所有Tile实例
  • 渲染时遍历列表调用每个Tile的Render(底层是spriteBatch.Draw)
  • 碰撞检测遍历视口内瓦片做矩形相交判断

核心优化:解决UpdateTiles的性能瓶颈

频繁重建Tile实例是性能暴跌的核心原因,每次创建新对象都会触发GC(垃圾回收),累积后导致卡顿。以下是针对性优化方案:

1. 改用对象池复用Tile实例

预先创建足够的Tile对象存入对象池,视口更新时仅复用闲置实例,避免重复初始化:

// Level类中新增对象池与活跃瓦片列表
private List<Tile> _tilePool = new List<Tile>();
private List<Tile> _activeTiles = new List<Tile>();
// 记录上次视口对应的瓦片范围,减少触发更新的频率
private int _lastXStart, _lastYStart, _lastXEnd, _lastYEnd;

private void UpdateTiles()
{
    Rectangle viewArea = Basic.camera.ViewArea;
    
    // 计算当前视口对应的瓦片范围
    int xStart = viewArea.X / Consts.TileWidth;
    int yStart = viewArea.Y / Consts.TileHeight;
    int xEnd = ((int)Math.Ceiling(viewArea.X + viewArea.Width) / Consts.TileWidth) + 1;
    int yEnd = ((int)Math.Ceiling(viewArea.Y + viewArea.Height) / Consts.TileHeight) + 1;

    // 仅当瓦片范围变化时才更新(避免像素级移动就触发重建)
    if (xStart != _lastXStart || yStart != _lastYStart || xEnd != _lastXEnd || yEnd != _lastYEnd)
    {
        // 标记所有活跃瓦片为闲置
        foreach (var tile in _activeTiles)
        {
            tile.IsActive = false;
        }

        // 钳位边界,避免越界访问地图
        xStart = Math.Max(0, xStart);
        yStart = Math.Max(0, yStart);
        xEnd = Math.Min(currentMap.GetLength(1), xEnd);
        yEnd = Math.Min(currentMap.GetLength(0), yEnd);

        for (int y = yStart; y < yEnd; ++y)
        {
            for (int x = xStart; x < xEnd; ++x)
            {
                int id = currentMap[y, x];
                if (id > 0)
                {
                    // 从对象池取闲置Tile,无闲置则新建
                    Tile tile = _tilePool.FirstOrDefault(t => !t.IsActive) 
                        ?? new Tile(Textures.tiles, Rectangle.Empty, TileType.None, 0, 0);
                    
                    // 更新Tile属性,复用实例
                    tile.Location = new Rectangle(x * Consts.TileWidth, y * Consts.TileHeight, Consts.TileWidth, Consts.TileHeight);
                    tile.Type = (TileType)id;
                    tile.GridX = x;
                    tile.GridY = y;
                    tile.IsActive = true;

                    // 新创建的Tile加入对象池
                    if (!_tilePool.Contains(tile))
                        _tilePool.Add(tile);
                    // 闲置Tile重新加入活跃列表
                    if (!_activeTiles.Contains(tile))
                        _activeTiles.Add(tile);
                }
            }
        }

        // 清理活跃列表中的闲置Tile
        _activeTiles.RemoveAll(t => !t.IsActive);
        
        // 更新记录的瓦片范围
        _lastXStart = xStart;
        _lastYStart = yStart;
        _lastXEnd = xEnd;
        _lastYEnd = yEnd;
    }
}

同时在Tile类中添加IsActive字段,用于标记实例状态:

public bool IsActive { get; set; }

2. 降低视口更新触发频率

原代码按像素级变化触发更新,相机轻微移动就会重建列表。改成按瓦片格子粒度判断,只有当视口对应的瓦片范围变化时才更新,大幅减少更新次数。


其他辅助优化

1. 渲染优化:批量绘制

避免每个Tile单独调用spriteBatch.Draw,统一在Level类中批量绘制,减少spriteBatch.Begin/End的调用开销:

private void RenderTiles()
{
    // 确保spriteBatch已处于Begin状态(在Level.Render方法开头调用)
    foreach (var tile in _activeTiles)
    {
        Basic.spriteBatch.Draw(tile.Texture, tile.Location, tile.SourceRect, Color.White);
    }
}

如果所有Tile使用同一图集纹理,批量绘制的性能提升会更明显。

2. 碰撞检测优化

原代码遍历所有活跃瓦片做碰撞判断,改成只检查玩家周围的瓦片,减少遍历数量:

private void Move(Vector2 v)
{
    // ... 原有代码 ...
    
    // 计算玩家所在的瓦片格子,扩大2格范围避免漏检
    int playerGridX = (int)(position.X / Consts.TileWidth);
    int playerGridY = (int)(position.Y / Consts.TileHeight);
    int checkXStart = Math.Max(0, playerGridX - 2);
    int checkXEnd = Math.Min(currentMap.GetLength(1), playerGridX + 2);
    int checkYStart = Math.Max(0, playerGridY - 2);
    int checkYEnd = Math.Min(currentMap.GetLength(0), playerGridY + 2);

    // 仅遍历玩家周围的碰撞瓦片
    List<Entity> collidingTiles = new List<Entity>();
    for (int y = checkYStart; y < checkYEnd; y++)
    {
        for (int x = checkXStart; x < checkXEnd; x++)
        {
            int id = currentMap[y, x];
            if (id > 0)
            {
                Tile tile = _activeTiles.FirstOrDefault(t => t.GridX == x && t.GridY == y);
                if (tile != null && tile.Collision && tile.BoundingBox.Intersects(collider))
                {
                    collidingTiles.Add(tile);
                }
            }
        }
    }

    // ... 后续碰撞处理代码 ...
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.20 20:57:04