这是我的第一篇文章,如果我犯了任何错误,请道歉。 如果在帖子的最后,您需要更多信息,那么我会更乐意发布更多信息。
我一直在研究基于2D平铺的游戏,我最近尝试了一种不同的方法来渲染平铺。但是现在我在渲染瓷砖时遇到了性能问题。
游戏的世界分为16 * 256个块,每个块都有自己的RenderTarget2D(16 * 256)来保存图块数据。我已经将渲染目标称为块缓冲区。
public void updateChunks()
{
int loadMinX = (int)( ( game.camera.location.X - game.GraphicsDevice.Viewport.Width / 2 ) / Tile.tileWidth / Chunk.chunkWidth );
int loadMaxX = (int)( ( game.camera.location.X + game.GraphicsDevice.Viewport.Width / 2 ) / Tile.tileWidth / Chunk.chunkWidth );
int minX = (int)( game.player.location.X / ( Chunk.chunkWidth * Tile.tileWidth ) ) - (int)Math.Floor( chunkCount / 2f );
int maxX = (int)( game.player.location.X / ( Chunk.chunkWidth * Tile.tileWidth ) ) + (int)Math.Floor( chunkCount / 2f );
Chunk currentChunk;
int cursorChunk = game.cursor.chunkLocation;
for( int c = minX; c <= maxX; c++ )
{
if( chunkBuffer.ContainsKey( c ) )
{
currentChunk = chunkBuffer[c];
if( c >= loadMinX && c <= loadMaxX )
{
currentChunk.bufferBuilt = false;
}
if( currentChunk.highlighted == true )
{
currentChunk.highlighted = false;
currentChunk.bufferBuilt = false;
}
if( chunkBuffer.ContainsKey( cursorChunk ) && chunkBuffer[cursorChunk] == currentChunk )
{
currentChunk.highlighted = true;
currentChunk.bufferBuilt = false;
}
if( currentChunk.bufferBuilt == false )
{
RebuildChunk( currentChunk );
}
}
}
game.tilesLoaded = chunkBuffer.Count * Chunk.chunkWidth * Chunk.chunkHeight;
}
每个tick都会调用此方法,并决定需要重建/重绘哪个块缓冲区。目前,视口中的块会在每个刻度上重建,并且鼠标所在的块也会重建(因为块会更改为突出显示的颜色。)
将块存储在字典中,并通过循环通过minX访问加载的块 - &gt; chunk的字典的minY值,如updateChunks()方法所示。
public void RebuildChunk( Chunk chunk )
{
BuildChunk( chunk );
chunk.bufferBuilt = true;
}
private void BuildChunk( Chunk chunk )
{
int chunkWidth = Chunk.chunkWidth;
int chunkHeight = Chunk.chunkHeight;
int chunkLocation = chunk.id * chunkWidth;
int loadMinX = (int)( ( game.camera.location.X - game.GraphicsDevice.Viewport.Width / 2 ) / Tile.tileWidth ) - 2;
int loadMaxX = (int)( ( game.camera.location.X + game.GraphicsDevice.Viewport.Width / 2 ) / Tile.tileWidth ) + 2;
int loadMinY = (int)( ( game.camera.location.Y - game.GraphicsDevice.Viewport.Height / 2 ) / Tile.tileHeight ) - 2;
int loadMaxY = (int)( ( game.camera.location.Y + game.GraphicsDevice.Viewport.Height / 2 ) / Tile.tileHeight ) + 2;
game.GraphicsDevice.SetRenderTarget( chunk.buffer );
game.GraphicsDevice.Clear( Color.CornflowerBlue );
game.spriteBatch.Begin( SpriteSortMode.Immediate, BlendState.NonPremultiplied );
for( int x = 0; x < chunkWidth; x++ )
{
for( int y = 0; y < chunkHeight; y++ )
{
if( x + chunkLocation > loadMinX && x + chunkLocation < loadMaxX && y > loadMinY && y < loadMaxY )
{
if( chunk.tiles[x, y].type == 0 && chunk.tiles[x, y].lightSource == false )
{
chunk.tiles[x, y].lightSource = true;
chunk.tiles[x, y].lightComponent = new Light( game, new Point( chunkLocation + x, y ), 1.0f, 6 );
}
if( chunk.tiles[x, y].lightSource == true )
{
if( chunk.tiles[x, y].lightComponent == null )
{
chunk.tiles[x, y].lightSource = false;
}
else
{
ApplyLighting( chunk, x, y, chunk.tiles[x, y].lightComponent );
}
}
float brightness = chunk.tiles[x, y].brightness;
if( chunk.tiles[x, y].type == 0 )
{
if( chunk.highlighted == true && game.cursor.tileLocation.X == x && game.cursor.tileLocation.Y == y )
{
game.spriteBatch.Draw( game.gameContent.airTexture, new Rectangle( x * Tile.tileWidth, y * Tile.tileHeight, Tile.tileWidth, Tile.tileHeight ), new Color( 0.5f, 0.5f, 0.5f ) );
}
else
{
game.spriteBatch.Draw( game.gameContent.airTexture, new Rectangle( x * Tile.tileWidth, y * Tile.tileHeight, Tile.tileWidth, Tile.tileHeight ), new Color( brightness, brightness, brightness ) );
}
}
else if( chunk.tiles[x, y].type == 1 )
{
if( chunk.highlighted == true && game.cursor.tileLocation.X == x && game.cursor.tileLocation.Y == y )
{
game.spriteBatch.Draw( game.gameContent.dirtTexture, new Rectangle( x * Tile.tileWidth, y * Tile.tileHeight, Tile.tileWidth, Tile.tileHeight ), new Color( 0.5f, 0.5f, 0.5f ) );
}
else
{
game.spriteBatch.Draw( game.gameContent.dirtTexture, new Rectangle( x * Tile.tileWidth, y * Tile.tileHeight, Tile.tileWidth, Tile.tileHeight ), new Color( brightness, brightness, brightness ) );
}
}
}
}
}
game.spriteBatch.End();
game.GraphicsDevice.SetRenderTarget( null );
}
RebuildChunk负责再次构建块并重置构建的标志,这样就不会浪费cpu时间来重建尚未编辑或不在视口中的块。
BuildChunk是将磁贴绘制到块缓冲区的位置。渲染目标被交换到块缓冲区,然后循环遍历块块。
if( x + chunkLocation > loadMinX && x + chunkLocation < loadMaxX && y > loadMinY && y < loadMaxY )
此行检查图块是否在视口中。 如果是,则检查以查看切片类型是什么,然后为该切片绘制相应的纹理。那里也有照明逻辑,但它不会影响我的问题。
我的主要渲染逻辑是:
protected override void Draw( GameTime gameTime )
{
frameCounter++;
drawScene( gameTime );
GraphicsDevice.Clear( Color.White );
spriteBatch.Begin( SpriteSortMode.Immediate, BlendState.NonPremultiplied );
spriteBatch.Draw( white, new Rectangle( 0, 0, GraphicsDevice.Viewport.Width, GraphicsDevice.Viewport.Height ), Color.CornflowerBlue );
spriteBatch.Draw( worldBuffer, new Rectangle( 0, 0, GraphicsDevice.Viewport.Width, GraphicsDevice.Viewport.Height ), Color.White );
spriteBatch.End();
debugUI.Draw( gameTime );
base.Draw( gameTime );
}
private void drawScene( GameTime gameTime )
{
int loadMinX = (int)( ( camera.location.X - GraphicsDevice.Viewport.Width / 2 ) / Tile.tileWidth / Chunk.chunkWidth );
int loadMaxX = (int)( ( camera.location.X + GraphicsDevice.Viewport.Width / 2 ) / Tile.tileWidth / Chunk.chunkWidth );
int minX = (int)( player.location.X / ( Chunk.chunkWidth * Tile.tileWidth ) ) - (int)Math.Floor( worldManager.chunkCount / 2f );
int maxX = (int)( player.location.X / ( Chunk.chunkWidth * Tile.tileWidth ) ) + (int)Math.Floor( worldManager.chunkCount / 2f );
GraphicsDevice.SetRenderTarget( worldBuffer );
GraphicsDevice.Clear( Color.CornflowerBlue );
spriteBatch.Begin( SpriteSortMode.Immediate, BlendState.NonPremultiplied, SamplerState.PointClamp, null, null, null, camera.GetViewTransformation( GraphicsDevice ) );
for( int c = minX; c <= maxX; c++ )
{
if( c >= loadMinX && c <= loadMaxX )
{
if( worldManager.chunkBuffer.ContainsKey( c ) )
{
Chunk currentChunk = worldManager.chunkBuffer[c];
if( currentChunk.bufferBuilt == true )
{
spriteBatch.Draw( currentChunk.buffer, new Rectangle( c * Chunk.chunkWidth * Tile.tileWidth, 0, Chunk.chunkWidth * Tile.tileWidth, Chunk.chunkHeight * Tile.tileHeight ), Color.White );
}
}
}
}
spriteBatch.Draw( player.material, new Rectangle( (int)player.location.X, (int)player.location.Y, 16, 32 ), Color.White );
spriteBatch.End();
GraphicsDevice.SetRenderTarget( null );
}
最后,在我的输入管理器中,我有以下内容:
if( mouseState.LeftButton == ButtonState.Pressed )
{
game.worldManager.RemoveTile( game.cursor.chunkLocation, game.cursor.tileLocation );
}
调用:
public void RemoveTile( int chunkLocation, Point location )
{
if( chunkBuffer.ContainsKey( chunkLocation ) && location.X >= 0 && location.X < Chunk.chunkWidth && location.Y >= 0 && location.Y < Chunk.chunkHeight )
{
if( chunkBuffer[chunkLocation].tiles[location.X, location.Y].type != 0 )
chunkBuffer[chunkLocation].tiles[location.X, location.Y].type = 0;
}
}
这只是我改变不同瓷砖状态的测试方法,在这种情况下,它会将污垢类型改为空气,以模拟去除瓷砖。
现在出现问题。当游戏第一次运行时,它很好,并保持在60fps。我将我的角色向右移动,用瓷砖填充屏幕宽度,然后我挖掘下来用瓷砖填充屏幕高度。
http://i.imgur.com/NBJ8qRj.png
http://i.imgur.com/N9i8asW.png (注意:第二张图片中的灯光已关闭)
在第一张图片中游戏仍然正常运行,但是当我开始在第二张图片中移除玩家周围的牌时,一段时间后帧率从60降到10以下并降到2-3 fps左右即使我已停止点击/对游戏做任何事情,也会永远保持这个速度并且无法恢复。
问题出现在全屏分辨率下,性能分析表明在BuildChunk()方法内的绘制调用期间会使用大量的cpu时间:
else
{
game.spriteBatch.Draw( game.gameContent.dirtTexture, new Rectangle( x * Tile.tileWidth, y * Tile.tileHeight, Tile.tileWidth, Tile.tileHeight ), new Color( brightness, brightness, brightness ) );
}
那么它可能是由于不断交换纹理而引起的吗?即;从绘制airTexture交换到dirtTexture等?因为我发现如果我将airTexture改为dirtTexture,问题就会消失。
如果我在我移除的所有瓷砖下面进一步挖掘,fps会恢复到60,所以我的猜测是它与在重建阶段绘制纹理有关。
如果有人可以查看这些代码,并指出任何有缺陷的内容,我们将不胜感激,并且可能的解决方案会很棒。
此外,如果某人有使用瓷砖的这种情况的经验,并提出您的建议将是非常宝贵的!
答案 0 :(得分:3)
我的天哪,这是一个彻底的问题!向我们提供大量信息是值得赞赏的,但将来尝试将您的问题简化为与直接相关的事物可能是一个好主意。否则人们会不堪重负。
我可以告诉你的是,我认为你的分析正处于正确的轨道上。正如您所猜测的,来回交换纹理可以对性能产生重大影响。
对2D渲染性能的最大限制之一是所谓的批量限制。基本上,每次你告诉图形设备绘制一些东西 - 也就是说,每次你调用一个DirectX的DrawFooPrimitives
函数 - 都会产生一定的固定开销。因此,制作大量的绘图调用,每个绘制调用只绘制少量多边形,效率非常低。你的GPU处于空闲状态,而你的CPU则会处理绘制调用!
XNA的SpriteBatch
类存在以解决此问题。它将大量精灵组装到单个缓冲区中,只需调用DrawIndexedPrimitives
即可绘制。但是,如果它们共享相同的图形设备状态,它只能批量精灵。这包括您在SpriteBatch.Begin()
中设置的采样器和光栅化器状态,但它还包括精灵纹理。更改纹理会强制SpriteBatch
刷新当前批次并启动一个全新的批处理。
那就是说,你正在犯另一个相关的错误,这种错误使得上述观点无关紧要。在您的渲染方法中,您使用SpriteBatch.Begin()
调用SpriteSortMode.Immediate
。对于大量的精灵而言,这将是非常低效的,因为你所做的就是说,“不要批量处理这些精灵;为我绘制的每一个精灵调用{em} {<1}} “。精灵是4个顶点。您的显卡每次DrawIndexedPrimitives
通话可能会处理数十万次。一个巨大的浪费!
总而言之,这是我在代码中会改变的内容:
Draw
更改为SpriteSortMode.Immediate
。使用此选项,SpriteSortMode.Deferred
将累积您绘制到缓冲区中的精灵,然后在您调用SpriteBatch
时立即绘制所有精灵。End()
中指定一个源矩形,它将告诉它仅使用较大图像的一小部分。