Your example is having one draw call per quad; this is going to kill performance with DX9 era MonoGame. The whole idea of SpriteBatch was to batch draw calls.
It is batched and culling by drawing a row of tiles at once and skipping thru the 1d array as it moves down a column so moving down is a draw but a full row is drawn all at once and culled on the sides before it is sent to the gpu.
I cleaned up the example a bit more and altered it so it can draw layers so basically 9 layers all alpha blended together + background, So bout 2 and a half to 3k tiles per frame at 400+ fps seems ok. Most people wont use 9 alpha blended full map layers for a map, but the trick here is that every tileset has a blank tile in it.
Id say just use the monogame extended or nkasts but i think he is trying to learn the basics to roll his own.
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
namespace HowToTileMap
{
/// <summary>
/// GL desktop project. has minor differences in positioning from dx.
/// </summary>
public class Example05TileLayeredCustomBufferedDraw : Game
{
public const int VERTICES_PER_QUAD = 6;
public const int TRIANGLES_PER_QUAD = 2;
SpriteFont font;
public static GraphicsDeviceManager graphics;
public static SpriteBatch spriteBatch;
public static Effect effect;
Matrix Projection;
public List<Texture2D> layerTextures = new List<Texture2D>();
public Texture2D t2d_height_mountain_mono;
public int mountainSourceTileWidth;
public int mountainSourceTileHeight;
public Texture2D t2d_rivers;
public int riverSourceTileWidth;
public int riverSourceTileHeight;
Vector2 mapViewedScrollPos = new Vector2();
private Vector3 camera2DScrollPosition = new Vector3(0, 0, -1);
private Vector3 camera2DScrollLookAt = new Vector3(0, 0, 0);
public float camera2dRotationZ = 0f;
public int mapWidth = 500;
public int mapHeight = 30;
public int tileSourceWidth;
public int tileSourceHeight;
public int tileDestinationWidth = 64;
public int tileDestinationHeight = 64;
public Point tilesThatFitToScreenSize;
// for our buffer
public List<VertexPositionColorTexture> vertexList = new List<VertexPositionColorTexture>();
VertexBuffer vertexBuffer;
public Example05TileLayeredCustomBufferedDraw()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
graphics.GraphicsProfile = GraphicsProfile.HiDef;
IsMouseVisible = true;
Window.AllowUserResizing = true;
graphics.PreferredBackBufferWidth = 1200;
graphics.PreferredBackBufferHeight = 900;
}
protected override void Initialize()
{
base.Initialize();
}
protected override void LoadContent()
{
spriteBatch = new SpriteBatch(GraphicsDevice);
// layer 0 tiles
t2d_height_mountain_mono = Content.Load<Texture2D>("height_mountain_mono");
mountainSourceTileWidth = t2d_height_mountain_mono.Width / 8;
mountainSourceTileHeight = t2d_height_mountain_mono.Height / 8;
// layer 1 tiles
t2d_rivers = Content.Load<Texture2D>("rivers");
riverSourceTileWidth = t2d_rivers.Width / 8;
riverSourceTileHeight = t2d_rivers.Height / 8;
// We set the default destination tile size to something constant as the source texture tile dimensions may change but the destination we want to stay the same.
// its better if the destination is larger then the source to prevent downsampling linear or if mip mapping is used.
// If it is smaller and you are not mip mapping use point instead of linear for the sampler state when you set states.
tileDestinationWidth = riverSourceTileWidth;
tileDestinationHeight = riverSourceTileWidth;
// this is dependant on the initial size of the screen at start up and is tied to the create of the vertices in the buffer so im not really handling resizing well here just enough.
tilesThatFitToScreenSize = new Point(GraphicsDevice.Viewport.Width / tileDestinationWidth + 2, GraphicsDevice.Viewport.Height / tileDestinationHeight + 2);
// ok so basically bunch of sheets bunch of tiles.
// Set layer 0 mountain to the primitive array.
CreateTileMapFromSheet(0, t2d_height_mountain_mono, mapWidth, mapHeight, tileDestinationWidth, tileDestinationHeight, mountainSourceTileWidth, mountainSourceTileHeight);
// Set layer 1 rivers to the primitive array.
CreateTileMapFromSheet(1, t2d_rivers, mapWidth, mapHeight, tileDestinationWidth, tileDestinationHeight, riverSourceTileWidth, riverSourceTileHeight);
// Set layer 2 rivers to the primitive array.
CreateTileMapFromSheet(2, t2d_rivers, mapWidth, mapHeight, tileDestinationWidth, tileDestinationHeight, riverSourceTileWidth, riverSourceTileHeight);
// Set layer 3 rivers to the primitive array.
CreateTileMapFromSheet(3, t2d_rivers, mapWidth, mapHeight, tileDestinationWidth, tileDestinationHeight, riverSourceTileWidth, riverSourceTileHeight);
// Set layer 4 rivers to the primitive array.
CreateTileMapFromSheet(4, t2d_rivers, mapWidth, mapHeight, tileDestinationWidth, tileDestinationHeight, riverSourceTileWidth, riverSourceTileHeight);
// Set layer 5 rivers to the primitive array.
CreateTileMapFromSheet(5, t2d_rivers, mapWidth, mapHeight, tileDestinationWidth, tileDestinationHeight, riverSourceTileWidth, riverSourceTileHeight);
// Set layer 6 rivers to the primitive array.
CreateTileMapFromSheet(6, t2d_rivers, mapWidth, mapHeight, tileDestinationWidth, tileDestinationHeight, riverSourceTileWidth, riverSourceTileHeight);
// Set layer 7 rivers to the primitive array.
CreateTileMapFromSheet(7, t2d_rivers, mapWidth, mapHeight, tileDestinationWidth, tileDestinationHeight, riverSourceTileWidth, riverSourceTileHeight);
// Set layer 8 rivers to the primitive array.
CreateTileMapFromSheet(8, t2d_rivers, mapWidth, mapHeight, tileDestinationWidth, tileDestinationHeight, riverSourceTileWidth, riverSourceTileHeight);
// so this is 9 layers and the clear color.
CreateBuffers();
SetBuffers();
effect = Content.Load<Effect>("TestingEffect");
effect.CurrentTechnique = effect.Techniques["QuadDraw"];
SetProjectionMatrix();
SetTexture(t2d_height_mountain_mono);
}
public void CreateTileMapFromSheet(int seed, Texture2D texture ,int mapWidth, int mapHeight, int tileDestinationWidth, int tileDestinationHeight, int tileWidth, int tileHeight)
{
// put the textures into a layer list to keep things orderly later in the draw loop.
layerTextures.Add(texture);
// we pass in a seed to randomize the map im not going to design a actual map for the example.
Random rnd = new Random(seed);
for (int y = 0; y < mapHeight; y++)
{
for (int x = 0; x < mapWidth; x++)
{
var rx = rnd.Next(0, 8);
var ry = rnd.Next(0, 8);
var source = new Rectangle(rx * tileWidth, ry * tileHeight, tileWidth, tileHeight);
var destination = new Rectangle(x * tileDestinationWidth, y * tileDestinationHeight, tileDestinationWidth, tileDestinationHeight);
CreateQuad(texture, destination, source);
}
}
}
private void CreateQuad(Texture2D texture, Rectangle destination, Rectangle source)
{
Vector2 tl = source.Location.ToVector2() / texture.Bounds.Size.ToVector2();
Vector2 tr = new Vector2(source.Right, source.Top) / texture.Bounds.Size.ToVector2();
Vector2 br = new Vector2(source.Right, source.Bottom) / texture.Bounds.Size.ToVector2();
Vector2 bl = new Vector2(source.Left, source.Bottom) / texture.Bounds.Size.ToVector2();
// t1
vertexList.Add(new VertexPositionColorTexture(new Vector3(destination.Left, destination.Top, 0f), Color.White, tl));
vertexList.Add(new VertexPositionColorTexture(new Vector3(destination.Left, destination.Bottom, 0f), Color.White, bl));
vertexList.Add(new VertexPositionColorTexture(new Vector3(destination.Right, destination.Bottom, 0f), Color.White, br));
// t2
vertexList.Add(new VertexPositionColorTexture(new Vector3(destination.Right, destination.Bottom, 0f), Color.White, br));
vertexList.Add(new VertexPositionColorTexture(new Vector3(destination.Right, destination.Top, 0f), Color.White, tr));
vertexList.Add(new VertexPositionColorTexture(new Vector3(destination.Left, destination.Top, 0f), Color.White, tl));
}
public void CreateBuffers()
{
var quads = vertexList.ToArray();
vertexBuffer = new VertexBuffer(GraphicsDevice, typeof(VertexPositionColorTexture), quads.Length, BufferUsage.WriteOnly);
vertexBuffer.SetData(quads);
}
public void SetCameraTileMapPixelPosition2D(Vector2 tilePosition, int currentTileMapDestinationWidth, int currentTileMapDestinationHeight)
{
var x = tilePosition.X * (float)currentTileMapDestinationWidth;
var y = tilePosition.Y * (float)currentTileMapDestinationHeight;
SetCameraPixelPosition2D( x, y);
}
private void SetCameraPixelPosition2D(float x, float y)
{
camera2DScrollPosition.X = x;
camera2DScrollPosition.Y = y;
camera2DScrollPosition.Z = -1;
camera2DScrollLookAt.X = x;
camera2DScrollLookAt.Y = y;
camera2DScrollLookAt.Z = 0;
}
public void SetStates()
{
GraphicsDevice.BlendState = BlendState.AlphaBlend; // AlphaBlend Opaque NonPremultiplied
GraphicsDevice.DepthStencilState = DepthStencilState.Default;
GraphicsDevice.SamplerStates[0] = SamplerState.PointClamp;
GraphicsDevice.RasterizerState = RasterizerState.CullClockwise;
}
public void SetUpEffect()
{
//effect.CurrentTechnique = effect.Techniques["QuadDraw"];
Viewport viewport = GraphicsDevice.Viewport;
Vector3 cameraUp = Vector3.TransformNormal(new Vector3(0, -1, 0), Matrix.CreateRotationZ(camera2dRotationZ)) * 10f;
Matrix World = Matrix.Identity;
Matrix View = Matrix.CreateLookAt(camera2DScrollPosition, camera2DScrollLookAt, cameraUp);
effect.Parameters["World"].SetValue(World);
effect.Parameters["View"].SetValue(View);
}
public void SetProjectionMatrix()
{
Viewport viewport = GraphicsDevice.Viewport;
Projection = Matrix.CreateScale(1, -1, 1) * Matrix.CreateOrthographicOffCenter(0, viewport.Width, viewport.Height, 0, 0, 1);
effect.Parameters["Projection"].SetValue(Projection);
}
public void SetTexture(Texture2D texture)
{
effect.Parameters["TextureA"].SetValue(texture);
}
public void SetBuffers()
{
GraphicsDevice.SetVertexBuffer(vertexBuffer);
}
protected override void UnloadContent()
{
}
protected override void Update(GameTime gameTime)
{
float elapsed = (float)gameTime.ElapsedGameTime.TotalSeconds;
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape))
Exit();
float scrollSpeedInTilesPerSecond = 10 * elapsed; // ten tiles per second so its kinda fast.
if (Keyboard.GetState().IsKeyDown(Keys.Left))
mapViewedScrollPos.X -= scrollSpeedInTilesPerSecond;
if (Keyboard.GetState().IsKeyDown(Keys.Right))
mapViewedScrollPos.X += scrollSpeedInTilesPerSecond;
if (Keyboard.GetState().IsKeyDown(Keys.Up))
mapViewedScrollPos.Y -= scrollSpeedInTilesPerSecond;
if (Keyboard.GetState().IsKeyDown(Keys.Down))
mapViewedScrollPos.Y += scrollSpeedInTilesPerSecond;
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear( Color.CornflowerBlue );
// We view the current scrolled position in pixels at.
SetCameraTileMapPixelPosition2D(mapViewedScrollPos, tileDestinationWidth, tileDestinationHeight);
// Were are we on the tile map and how many tiles do we draw in relation to the scrolled world position.
var mapVisibleDrawRect = new Rectangle((int)mapViewedScrollPos.X, (int)mapViewedScrollPos.Y, tilesThatFitToScreenSize.X, tilesThatFitToScreenSize.Y);
// set states and effect up.
SetStates();
SetUpEffect();
// Draw all tiles that are valid from start to end layers.
DrawTileMap(mapVisibleDrawRect, 0 , layerTextures.Count);
base.Draw(gameTime);
}
public void DrawTileMap(Rectangle TileRegionToDraw, int StartingLayer, int LayerLength)
{
// The total map tiles
int mapTilesLength = mapWidth * mapHeight;
// Ensure the buffer is set we could have different buffers for each layer but instead i put it all in one buffer.
SetBuffers();
foreach (EffectPass pass in effect.CurrentTechnique.Passes)
{
// Loop thru the layers we are going to draw.
for (int layer = StartingLayer; layer < (StartingLayer + LayerLength); layer++)
{
// Set the texture that this layer uses on the card.
SetTexture(layerTextures[layer]);
// Call apply the set the texture.
pass.Apply();
// Calculate the layer offset.
var layerVerticeOffset = mapTilesLength * layer * VERTICES_PER_QUAD;
// Increment down a column as we draw each row of tiles that is in view.
for (int y = TileRegionToDraw.Y; y < TileRegionToDraw.Bottom; y++)
{
// Calculate the starting index position for this row of tiles at the targeted layer within the 1 dimensional triangle buffer and the span of primitives to draw.
int startingVerticeIndex = ((y * mapWidth + TileRegionToDraw.X) * VERTICES_PER_QUAD) + layerVerticeOffset;
int primitiveSpanLength = TileRegionToDraw.Width * TRIANGLES_PER_QUAD;
// Tell the gpu to execute the drawing of this visbile row span of tiles in the shader.
GraphicsDevice.DrawPrimitives(PrimitiveType.TriangleList, startingVerticeIndex, primitiveSpanLength);
}
}
}
}
}
}
same shader as above.
Images used.