I've been working on drawing an isometric map with C# / XNA Game Studio, and while I've gotten pretty far it doesn't look quite right and I was wondering if anybody can help.
Here's the code I have for drawing the map:
public void Draw(SpriteBatch theBatch, int drawX, int drawY)
{
if ((drawY % 2 == 0))
theBatch.Draw(tileTexture, new Rectangle((drawX * width), (drawY * length / 2), width, length), Color.White);
else
theBatch.Draw(tileTexture, new Rectangle(((drawX * width) + (width / 2)), (drawY * length / 2), width, length), Color.White);
}
The code within this method acts as if it were inside a nested for loop, drawing left to right, top to bottom. When the y-value is odd, the row is shifted over to fit, however it looks a bit off.
This is the produced output for an 8x5 map:
As you can see, it doesn't quite look right, and I'm not sure if its an issue with the math in my code, or if it has to do with the order everything is being drawn in. I'm very new to C# and working with sprites, so any help would be greatly appreciated.
Because it might be helpful, here is the other relevant parts of code which draw the map.
The entire Tile Class:
public class Tile
{
// Dimension variables
int height;
int width;
int length;
String type;
Texture2D tileTexture;
Vector2 coordinates;
///
/// Tile Constructor
///
public Tile(ContentManager theContent, String theType, Vector2 theCoordinates)
{
width = 68;
length = 46;
type = theType;
coordinates = theCoordinates;
// Sets the right texture to the texture ref
if (theType == "grass")
tileTexture = theContent.Load<Texture2D>(#"Tiles\iso_grass");
}
///
/// Draws the tile at the given location
///
public void Draw(SpriteBatch theBatch, int drawX, int drawY)
{
if ((drawY % 2 == 0))
theBatch.Draw(tileTexture, new Rectangle((drawX * width), (drawY * length / 2), width, length), Color.White);
else
theBatch.Draw(tileTexture, new Rectangle(((drawX * width) + (width / 2)), (drawY * length / 2), width, length), Color.White);
}
}
The TileRow class, which holds one row of tiles.
public class TileRow
{
public List<Tile> Row = new List<Tile>();
public int rowLength;
public TileRow(int theLength, int yIndex, ContentManager theContent)
{
rowLength = theLength;
Tile thisTile;
// Here the tiles are created and added to the row
for (int x = 0; x < rowLength; x++)
{
thisTile = new Tile(theContent, "grass", new Vector2(x, yIndex));
Row.Add(thisTile);
}
}
///
/// Draw -- invokes the draw method of each tile in the row
///
public void DrawRow(SpriteBatch theBatch, int currentY)
{
for (int x = 0; x < rowLength; x++)
{
Row[x].Draw(theBatch, currentY, x);
}
}
}
}
and the MapStruct class, which holds all the rows
public class MapStruct
{
public List<TileRow> allRows = new List<TileRow>();
int depth;
// Constructor
public MapStruct(ContentManager theContent, int theDepth, int rowLength)
{
depth = theDepth;
TileRow thisRow;
// Here we make a row of tiles for each level of depth
for (int y = 0; y < depth; y++)
{
thisRow = new TileRow(rowLength, depth, theContent);
allRows.Add(thisRow);
}
}
///
/// Draw - this method invokes the draw method in each tile row, which then draws each tile
///
public void DrawMap(SpriteBatch theBatch)
{
for (int y = 0; y < depth; y++)
{
allRows[y].DrawRow(theBatch, y);
}
}
}
Any help on how I could fix this issue, as well as advice on how I could improve my code would be greatly appreciated!
Looks like your loop adds a little to much to the Y on each row.
I found this variable in your Tile function.
length = 46;
I havent checked, but I believe "length" is the height of the tile? if so, try ajusting it a bit. Perhaps, you've forgotten to minus the height of the tile. So if the side of the tile is like 6 pixels, then the length for offset pr. row is only 40.
Also remember to plot from top and down, since the tiles nearest camera has to be plotted last, to make the depth illusion.
I beleive BerggreenDK is right, but your comment make it seem you misunderstood his answer. Your tiles need to be drawn at an Y offset that only includes the green surface areas "screen height". So, draw the full tile size, but offset the rows with length - 5 (Where 10 is the estimated offset and 5 is half that, as each row should be offset by only half the distance).
theBatch.Draw(tileTexture, new Rectangle((drawX * width), (drawY * (length) / 2), width, length - 5), Color.White);
...if I got the parameters right.
Also, the rows need to be drawn from back to front, but they seem to be drawn from left to right? I don't know XNA, so can't show code to fix that...when I look closely some tiles in a row "further away" is over-drawn by a tile "closer". I don't know XNA and don't get how the drawing order is determined, so can't offer a fix for that...
Related
Alright, so today I decided to try to further optimize my collision detection code for my tile engine.
This is what I did:
Circle class checks if there are points within range. If there are, then check for collision between player and tile.
Code:
int tileWidth = 128;
int tileHeight = 128;
int[,] Layer3 = { 1, 1, 1, etc... };
int tileMapWidth = Layer3.GetLength(1);
int tileMapHeight = Layer3.GetLength(0);
Rectangle tile, tile2;
for (int x = 0; x < tileMapWidth; x++)
{
for (int y = 0; y < tileMapHeight; y++)
{
int wallIndex = Layer3[y, x];
if (wallIndex == 1) //Full-sized Tile Collision (128 x 128)
{
if (collisionCircle.Contains(new Vector2(x * tileWidth + (tileWidth / 2) + (int)Player.camera.Position.X,
y * tileHeight + (tileHeight / 2) + (int)Player.camera.Position.Y))) //+ tile / 2 is for centering the point
{
tile = new Rectangle(x * tileWidth + (int)Player.camera.Position.X, y * tileHeight + (int)Player.camera.Position.Y, tileWidth, tileHeight);
Collide(tile);
}
}
}
}
This would check throughout layer3 if there is a "1". If there is, assign rectangle and check for collision if point is inside collision radius.
Also, I checked this code(with a draw method), and I know it's working properly, at least the behavior.
I added in about 120,000(32 x 3888) tiles to try to make it lag, and before the code, it lagged a little bit. But after I added in the code, it lagged even more so.
I thought that since it would only check for collision between tiles(points) that are within the radius it wouldn't even remotely lag, but that's not the case...
Any help/ideas on how to optimize this would be great.
Thanks a lot,
Shyy
EDIT:
Cirlce.Contains() code:
public bool Contains(Vector2 Point)
{
return ((Point - position).Length() <= radius);
}
I used a circle because I've heard it's faster than using a rectangle.
Another possible optimization is instead of
return ((Point - position).Length() <= radius);
use
return ((Point - position).LengthSquared() <= radius * radius);
This is faster because Vector2.Length() has to perform a costly square root operation. Vector2.LengthSquared() does not have to perform that slow operation. The radius has to be multiplied by itself to account for the length from the vector being squared.
It sounds like you're trying to determine what tiles you don't need to use for collision with the player. Another optimization you could do is that if a tile at (X=5,Y=5) is above and to the left of the player, then you don't need to check a tile at (X=4,Y=4). Similarly if (X=5,Y=5) is below and to the right, (X=6,Y=6) is guaranteed to be too far as well. Try to determine when you've passed the player and no longer need to check collisions.
I suggest to loop only over visible tiles in screen to check collision using movement offset.
i will try something from my head..
for x as integer = 0 + offSetX to tilesInWidth + offSetX
for y as integer = 0 + offSetY to tilesInHeight + offSetY
if player.insideCircle(player.position, radius) '
object = layer(y,x);
if player.collideWith(object) then Collide()
end if
next
next
just wanna ask if How can I draw series of Numbers for every line of grid col and rows ...
can also be by use of label ...
something like this:
http://oi60.tinypic.com/aeblth.jpg
heres my code as for now for Grid of PictureBox:
private void pictureBox1_Paint(object sender, PaintEventArgs e)
{
Graphics g = e.Graphics;
int numOfCells = 200;
int cellSize = 5;
Pen p = new Pen(Color.Black);
for (int y = 0; y < numOfCells; ++y)
{
g.DrawLine(p, 0, y * cellSize, numOfCells * cellSize, y * cellSize);
}
for (int x = 0; x < numOfCells; ++x)
{
g.DrawLine(p, x * cellSize, 0, x * cellSize, numOfCells * cellSize);
}
}
Note that numbers in the bottom and right of chart Position and count
are depends on count and position of line of grid in picture .
thanks for the help . more power!
First you have to create a block of X and Y. In the given screen shot the block of Y is about 10 and the block of X is about 2. To create the block of axes you need maximum and minimum value of graph.
NOTE: This is not a complete example. This is just a logic to understand how you can draw graph line.
int iXMin = 0;
int iXMax = 52;
int iYMin = 49890;
int iYMax = 50000;
Then you should create the size of block.
int iXSize = 26;
int iYSize = 12;
Single fXBlock = (iXMax - iXMin) / iXSize;
Single fYBlock = (iYMax - iYMin) / iYSize;
Now, You will require a method to convert axes position into pixel position.
This method is used to get the point of axes.
Single fXPxlSize = (pic.ClientRectangle.Width / (iXMax-iXMin))); //Getting X pixle size between two value.
Single fYPxlSize = (pic.ClientRectangle.Height / (iYMax-iYMin)); //Getting Y pixle size between two value.
Now, You can point out the value in the PictureBox.
Suppose you have first value XValue=0,YValue=50000 then you can get pixel position by using following formula.
int iX = Convert.ToInt32((XValue - XMin) * fXPxlSize);
int iY = Convert.ToInt32((YValue - YMin) * fYPxlSize);
Point p = New Point(iX,iY);
Create two List then add all those points one by one into the list. First is used to draw Cyan Line and Second is used to draw Yellow Line
List<Point> lstPointsC = new List<Point>(); //Declaration should be class level.
List<Point> lstPointsY = new List<Point>(); //Declaration should be class level.
lstPointsC.Add(p);
if (lstPointsY.Count > 0)
lstPointsY.Add(new Point(iX, lstPointsY[lstPointsY.Count].Y));
lstPointsB.Add(p);
Create code for Lines.
Graphics g = pic.CreateGraphics(); //you can also use e.Graphics from pic_paint event.
g.DrawLines(Pens.Cyan, lstPointsC.ToArray());
g.DrawLines(Pens.Yellow, lstPointsY.ToArray());
This method is used to draw custom line graph in your own control or form. But, I would like to suggest you use any third party tool like Crystal Report or Devexpress Charts.
I'm a bit mind boggled at this, but i have the problem of trying to get my head around mouse snapping to a grid. Currently I'm drawing a grid by overriding OnRender like so;
protected override void OnRender(DrawingContext drawingContext)
{
int numberOfVerticalLines = 8;
int numberOfHorizontalLines = 8;
CellHeight = this.ActualHeight / numberOfVerticalLines;
CellWidth = this.ActualWidth / numberOfHorizontalLines;
double verticalOffset = 0;
double horizontalOffset = 0;
Pen pen = new Pen(Stroke, StrokeThickness);
pen.DashStyle = DashStyle;
for (int i = 0; i <= numberOfHorizontalLines; i++)
{
for (int j = 0; j <= numberOfVerticalLines; j++)
{
drawingContext.DrawLine(pen, new Point(horizontalOffset, verticalOffset), new Point(horizontalOffset, CellHeight + verticalOffset));
verticalOffset += CellHeight;
}
horizontalOffset += CellWidth;
verticalOffset = 0;
}
horizontalOffset = 0;
verticalOffset = 0;
for (int i = 0; i <= numberOfVerticalLines; i++)
{
for (int j = 0; j <= numberOfHorizontalLines; j++)
{
drawingContext.DrawLine(pen, new Point(horizontalOffset, verticalOffset), new Point(CellWidth + horizontalOffset, verticalOffset));
horizontalOffset += CellWidth;
}
verticalOffset += CellHeight;
horizontalOffset = 0;
}
}
And that gives the following result;
However i'm a little stuck with thinking about a route to take to snap the mouse to the nearest grid intersection (where a horizontal line meets a vertical line). Obviously as i'm using the drawingcontext to draw the lines i have no reference to these lines after they've been drawn.
So i guess essentially my question is, how can i go about implementing mouse snap to grid? Is this more of a maths question than an object orientated control question? I've read through almost every stack overflow question i found relevant but havn't been able to come to any realistic ideas yet.
Note: Whilst at the minute i've hard coded an 8x8 grid, this will be user defined eventually.
A basic approach is to compare the (x,y) of your mouse to the crosses by:
1. calculating the start- and endpoint of the width and height of the cell in which the mouse is located; and
2. comparing those two intervals (width and height) to the actual mouse (x,y) to find the nearest cell point.
Here is some quickly thrown together example code to demonstrate the snapping:
/// <summary>
/// When left shift key is pressed we snap the mouse to the closest
/// intersection
/// </summary>
void MainWindow_KeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.LeftShift)
{
var p = GetSnappingPoint(Mouse.GetPosition(this), new Size(200, 200));
SetCursorPos((int)p.X, (int)p.Y+20);
}
}
[DllImport("User32.dll")]
private static extern bool SetCursorPos(int x, int y);
/// <summary>
/// Get snapping point by
/// </summary>
Point GetSnappingPoint(Point mouse,Size cellSize)
{
//Get x interval based on cell width
var xInterval = GetInterval(mouse.X, cellSize.Width);
//Get y interal based in cell height
var yInterval = GetInterval(mouse.Y, cellSize.Height);
// return the point on cell grid closest to the mouseposition
return new Point()
{
X = Math.Abs(xInterval.Lower - mouse.X) > Math.Abs(xInterval.Upper - mouse.X) ? xInterval.Upper : xInterval.Lower,
Y = Math.Abs(yInterval.Lower - mouse.Y) > Math.Abs(yInterval.Upper - mouse.Y) ? yInterval.Upper : yInterval.Lower,
};
}
/// <summary>
/// Find an interval of the celsize based on mouse position and size
/// </summary>
Interval GetInterval(double mousePos,double size)
{
return new Interval()
{
Lower = ((int)(mousePos / size)) * size,
Upper = ((int)(mousePos / size)) * size + size
};
}
/// <summary>
/// Basic interval class
/// </summary>
class Interval
{
public double Lower { get; set; }
public double Upper { get; set; }
}
A rough start to an answer:
int nearGridX = CellWidth * Math.Round( mouseX / CellWidth);
int nearGridY = CellHeight * Math.Round( mouseY / CellHeight);
xPos= mouseX - (mouseX % gridWidth);
yPos= mouseY - (mouseY % gridHeight);
This would give you a quick and dirty snap to the upper-left corner of the current grid. This doesn't take into account for if you're closer to the bottom or right of the current grid. It only looks at what grid you're in and plops it there.
This method that draws my tiles seems to be quite slow, Im not sure exactly whats wrong, it belive my culling method isnt working and is drawing stuff offscreen, but im not completeley sure. Here it is:
// Calculate the visible range of tiles.
int left = (int)Math.Floor(cameraPosition.X / 16);
int right = left + spriteBatch.GraphicsDevice.Viewport.Width / 16;
right = Math.Min(right, Width) + 1; // Width -1 originally - didn't look good as tiles drawn on screen
if (right > tiles.GetUpperBound(0))
right = tiles.GetUpperBound(0) + 1; // adding 1 to get the last right tile drawn
int top = (int)Math.Floor(cameraPosition.Y / 16);
int bottom = left + spriteBatch.GraphicsDevice.Viewport.Height/ 16;
bottom = Math.Min(bottom, Height) + 1; // Height -1 originally - didn't look good as tiles drawn on screen
if (bottom > tiles.GetUpperBound(1))
bottom = tiles.GetUpperBound(1) + 1; // adding 1 to get the last bottom tile drawn
// For each tile position
for (int y = top; y < bottom; ++y)
{
for (int x = left; x < right; ++x)
{
// If there is a visible tile in that position, draw it
if (tiles[x, y].BlockType.Name != "Blank")
{
Texture2D texture = tileContent["DirtBlock_" + getTileSetType(tiles,x,y)];
spriteBatch.Draw(texture, new Vector2(x * 16, y * 16), Color.White);
if (isMinimap)
spriteBatch.Draw(pixel, new Vector2(30+x, 30+y), Color.White);
}
}
}
GetTileSetTypes is a function to get what tiles are around it, for different textures, like DirtBlock_North, DirtBlock_Center, etc.
Tile content is just a class with my block textures.
Try changing SpriteBatch.Begin to defered and combining all of the tiles onto one texture.
See this GameDev question for info about why deferred is most likely the fastest option for you.
Also realize that every time you draw a new texture you have to take the old one out of the GPU and put the new one in. This process is called texture swapping and usually isn't an issue but you are swapping textures twice per tile which is likely to impact performance noticeably.
This can be fixed by combining multiple sprites onto one texture and using the source rectangle argument. This allows you to draw multiple sprites without a texture swap. There are a few OSS libraries for this. Sprite Sheet Packer is my personal favorite.
Unfortunantly without the project and a profiler I'm just guessing; however, these are the two biggest gotchas for rendering tilemaps I know of. I can't really see anything wrong from here. Below is the code I use to draw my tile maps and as you see its very similar to yours.
If all else fails I would suggest using a profiler to figure out which bits are running slowly.
//Init the holder
_holder = new Rectangle(0, 0, TileWidth, TileHeight);
//Figure out the min and max tile indices to draw
var minX = Math.Max((int)Math.Floor((float)worldArea.Left / TileWidth), 0);
var maxX = Math.Min((int)Math.Ceiling((float)worldArea.Right / TileWidth), Width);
var minY = Math.Max((int)Math.Floor((float)worldArea.Top / TileHeight), 0);
var maxY = Math.Min((int)Math.Ceiling((float)worldArea.Bottom / TileHeight), Height);
for (var y = minY; y < maxY; y++) {
for (var x = minX; x < maxX; x++) {
_holder.X = x * TileWidth;
_holder.Y = y * TileHeight;
var t = tileLayer[y * Width + x];
spriteBatch.Draw(
t.Texture,
_holder,
t.SourceRectangle,
Color.White,
0,
Vector2.Zero,
t.SpriteEffects,
0);
}
}
I'm working on a tile/sprite-based game, calling my Draws via
public void Draw(SpriteBatch spriteBatch)
{
DrawBehind(spriteBatch);
DrawEven(spriteBatch);
DrawInFront(spriteBatch);
}
private void DrawBehind(SpriteBatch spriteBatch)
{
spriteBatch.Begin();
ActiveMap.DrawAtDepth(spriteBatch, Player, 1);
spriteBatch.End();
}
private void DrawEven(SpriteBatch spriteBatch)
{
spriteBatch.Begin(SpriteSortMode.BackToFront, null);
ActiveMap.DrawAtDepth(spriteBatch, Player, -1);
Player.Draw(spriteBatch);
spriteBatch.End();
}
private void DrawInFront(SpriteBatch spriteBatch)
{
spriteBatch.Begin();
ActiveMap.DrawAtDepth(spriteBatch, Player, 0);
spriteBatch.End();
}
As expected, DrawBehind and DrawInFront just draw based on the order they are called, stable and well on both X and Y.
DrawEven sorts on the Y-axis correctly, but on the X-axis, the sprites just seem to flip in front of eachother and back for no apparent reason.
If it would be stable, I don't really care in which order it sorts it. Instead, everytime I "move my character" (move the position of all the sprites but my character's), it flickers and switches all around. Anyone knows what causes this or how to fix it?
As per request
EDIT:
Map:
public void DrawAtDepth(SpriteBatch spriteBatch, Player player, int depth)
{
Area.DrawAtDepth(spriteBatch, player, depth);
}
Area:
public void DrawAtDepth(SpriteBatch spriteBatch, Player player, int depth)
{
const int border = 10;
var screenTiles = new Rectangle(
0 - border,
10 - border,
(GameMain.ScreenRectangle.Width / Tile.Width - 1) + border,
(GameMain.ScreenRectangle.Height / Tile.Height - 1) + 10 + border);
var playerLevelPosition = player.LevelPosition;
for (int layer = 0; layer < _layerCount; layer++)
if (ActiveLevels[1, 1].GetLayerDepth(layer) == depth)
for (int y = screenTiles.Y; y < screenTiles.Height; y++)
for (int x = screenTiles.X; x < screenTiles.Width; x++)
Draw(spriteBatch, player, playerLevelPosition, layer, x, y);
}
private void Draw(SpriteBatch spriteBatch, Player player, Vector2 playerLevelPosition, int layer, int x, int y)
{
int levelX;
var tileX = TilePos((int)playerLevelPosition.X, Level.Width, x, out levelX);
int levelY;
var tileY = TilePos((int)playerLevelPosition.Y, Level.Height, y, out levelY);
var level = ActiveLevels[levelX, levelY];
if (level != null)
level.Draw(spriteBatch, player, layer, new Vector2(tileX, tileY));
}
private float TilePos(int playerPos, int mapBounds, int offset, out int levelPos)
{
var tilePos = ((playerPos) - (mapBounds/2f - 1)) + offset;
levelPos = 1;
if (tilePos < 0)
{
levelPos = 0;
return tilePos + mapBounds;
}
if (tilePos >= mapBounds)
{
levelPos = 2;
return tilePos - mapBounds;
}
return tilePos;
}
public static float GetDepth(float tilePos)
{
return 1 - tilePos / (Level.Height * 3);
}
Layer:
public void Draw(SpriteBatch spriteBatch, Vector2 tile, Vector2 drawPosition, Color color, bool transparent = false)
{
if (_grid[(int)tile.X, (int)tile.Y] == null) return;
_grid[(int)tile.X, (int)tile.Y].Draw(spriteBatch, drawPosition, color, transparent, (Depth != -1) ? Depth : Area.GetDepth(Level.Height + tile.Y));
}
Tile:
public override void Draw(SpriteBatch spriteBatch, Vector2 drawPosition, Color color, bool transparent = false, float depth = 0)
{
if (!GameMain.ScreenRectangle.Intersects(_sprite.Bounds)) return;
if (transparent && _fade)
color = new Color(255, 255, 255, 100);
spriteBatch.Draw(
_sprite,
drawPosition,
null,
color,
0,
new Vector2(_sprite.Width / 2 - Width / 2, _sprite.Height),
1,
SpriteEffects.None,
depth
);
}
Also, any general tips for the sake of it are welcome. I'm a pretty new programmer, styling tips are appreciated.
I'm not completely familiar with the latest XNA 4, so this might not be -the- correct answer, but:
From the symptoms you describe and the fact that it's only happening when you set the BackToFront SpriteSortMode, I would expect that you're either not setting a depth coordinate for some of the sprites being drawn in DrawEven or not making them different enough. As such, I believe that it is trying to override the normal deferred render order, but doesn't have enough depth information and just picks an arbitrary order each frame (perhaps due to floating point inaccuracy? Not certain, but it seems likely.)
Since you mention it's sorting properly on the Y axis, then assuming you're using the Y coordinate as an input to the layer depth, try adding a very small amount of the X coordinate as well. Assuming 32 by 32 tiles on screen, I would calculate the layer depth floating point value like this:
float depth = (someSprite.ScreenYCoord / 32) + (someSprite.ScreenXCoord / 1025);
spriteBatch.Draw(texture, position, sourceRect, color, rotation, origin, scale, spriteEffects, depth);
where 1025 is 32 * 32 + 1 (to make sure that a difference of 1 tile in Y is always more important when sorting than even a full 32 tiles difference in X) and assuming a zero-based screen coordinate system where something can be on tile 0 to 31 in either axis.
Also as far as I can tell ActiveMap.DrawAtDepth is not an XNA library function. Can you show us the code for that please, or show us wherever else the actual calls to SpriteBatch.Draw() are - that would help people work out what exactly is happening, and hopefully mean we have to guess less.
The idea of spritebatch Begin() and End() is to minimize the amount of calls to it to increase performace, so it was concievedthat everything between 2 of them will be drawn in order. Change your code to this:
spriteBatch.Begin();
DrawBehind(spriteBatch);
DrawEven(spriteBatch);
DrawInFront(spriteBatch);
spriteBatch.End();
and remove all instances of Begin() and End() in those functions.
If that doesn't help, check how ActiveMap.DrawAtDepth() works, but that should improve your program effciency in any case.