Here is a snippet from my tile engine. This snippet only uses 1 texture but you should get the idea.
CScreenGrid.cpp
Code:
#include "CScreenGrid.h"
const DWORD TLVertex::FVF=D3DFVF_XYZRHW |
D3DFVF_DIFFUSE | D3DFVF_TEX1;
void CScreenGrid::Create(IDirect3DDevice9 *Device,std::string File)
{
DebugFile.Create("CScreenGrid.log","CScreenGrid Debug Dump");
DebugFile.File << "Loading texture...";
if (FAILED(D3DXCreateTextureFromFile(Device,File.c_str(),&m_pTexture)))
{
MessageBox(0,"Failed to load texture",0,0);
return;
}
DebugFile.File << "SUCCESS" << endl;
m_pDevice=Device;
//Create vertex buffer
DebugFile.File << "Calculating grid extents" << endl;
DebugFile.File << "************************" << endl;
m_dwTotalTiles=m_iNumTilesHoriz*m_iNumTilesVert;
m_dwTotalVerts=m_dwTotalTiles*6;
m_dwTotalTriangles=m_dwTotalTiles*2;
DebugFile.File << "Total tiles:" << m_dwTotalTiles << endl;
DebugFile.File << "Total verts:" << m_dwTotalVerts << endl;
DebugFile.File << "Total tris:" << m_dwTotalTriangles << endl;
DebugFile.File << "XIncrement:" << m_iIncrementX << endl;
DebugFile.File << "YIncrement:" << m_iIncrementY << endl << endl;
DebugFile.File << "Creating vertex buffer with " << m_dwTotalVerts << " vertexes...";
HRESULT hr=Device->CreateVertexBuffer(m_dwTotalVerts*sizeof(TLVertex),
D3DUSAGE_WRITEONLY,
TLVertex::FVF,
D3DPOOL_MANAGED,
&m_pVB,
0);
if (hr!=D3D_OK)
{
DebugFile.File << "FAILED with " ;
switch (hr)
{
case D3DERR_INVALIDCALL: DebugFile.File << "D3DERR_INVALIDCALL" << endl;break;
case D3DERR_OUTOFVIDEOMEMORY: DebugFile.File << "D3DERR_OUTOFVIDEOMEMORY" << endl;break;
case E_OUTOFMEMORY: DebugFile.File << "D3DERR_OUTOFMEMORY" << endl;break;
}
return;
} else DebugFile.File << "SUCCESS" << endl;
DebugFile.File << "Locking vertex buffer in prep for write to card...";
TLVertex *Vertices;
hr=m_pVB->Lock(0,0,(void **)&Vertices,0);
if (hr!=D3D_OK)
{
DebugFile.File << "FAILED" << endl;
return;
} else DebugFile.File << "SUCCESS" << endl;
//Push vertices out to card
int vertexnum=0;
float vx=(float)-m_iIncrementX;
float vy=(float)-m_iIncrementY;
DebugFile.File << "Entering vertex loop" << endl;
int quad=0;
for (int i=0;i<m_iNumTilesVert;i++)
{
for (int j=0;j<m_iNumTilesHoriz;j++)
{
Vertices[vertexnum]=TLVertex(vx,vy,1.0f,0.0f,0.0f);
Vertices[vertexnum].diffuse=D3DXCOLOR(1.0f,1.0f,1.0f,1.0f);
Vertices[vertexnum+1]=TLVertex(vx+(float)m_iIncrementX,vy,1.0f,1.0f,0.0f);
Vertices[vertexnum+1].diffuse=D3DXCOLOR(1.0f,1.0f,1.0f,1.0f);
Vertices[vertexnum+2]=TLVertex(vx,vy+(float)m_iIncrementY,1.0f,0.0f,1.0f);
Vertices[vertexnum+2].diffuse=D3DXCOLOR(1.0f,1.0f,1.0f,1.0f);
Vertices[vertexnum+3]=TLVertex(vx+(float)m_iIncrementX,vy,1.0f,1.0f,0.0f);
Vertices[vertexnum+3].diffuse=D3DXCOLOR(1.0f,1.0f,1.0f,1.0f);
Vertices[vertexnum+4]=TLVertex(vx+(float)m_iIncrementX,vy+(float)m_iIncrementY,1.0f,1.0f,1.0f);
Vertices[vertexnum+4].diffuse=D3DXCOLOR(1.0f,1.0f,1.0f,1.0f);
Vertices[vertexnum+5]=TLVertex(vx,vy+(float)m_iIncrementY,1.0f,0.0f,1.0f);
Vertices[vertexnum+5].diffuse=D3DXCOLOR(1.0f,1.0f,1.0f,1.0f);
DebugFile.File << "Vertices for quad " << quad << endl;
DebugFile.File << "************************" << endl;
DebugFile.File << "Current X,Y is " << vx << "," << vy << endl;
DebugFile.File << "Vertex #" << vertexnum << ": ";
DebugFile.File << vx << "," << vy << ",1.0f,0.0f,0.0f" << endl;
DebugFile.File << "Vertex #" << (vertexnum+1) << ": ";
DebugFile.File << vx+(float)m_iIncrementX << "," << vy << ",1.0f,1.0f,0.0f" << endl;
DebugFile.File << "Vertex #" << (vertexnum+2) << ": ";
DebugFile.File << vx << "," << vy+(float)m_iIncrementY << ",1.0f,0.0f,1.0f" << endl;
DebugFile.File << "Vertex #" << (vertexnum+3) << ": ";
DebugFile.File << vx+(float)m_iIncrementX << "," << vy << ",1.0f,1.0f,0.0f" << endl;
DebugFile.File << "Vertex #" << (vertexnum+4) << ": ";
DebugFile.File << vx+(float)m_iIncrementX << "," << vy+(float)m_iIncrementY << ",1.0f,1.0f,1.0f" << endl;
DebugFile.File << "Vertex #" << (vertexnum+5) << ": ";
DebugFile.File << vx << "," << vy+(float)m_iIncrementY << ",1.0f,0.0f,1.0f" << endl << endl;
quad++;
vertexnum+=6;
vx+=(float)m_iIncrementX;
}
vx=(float)-m_iIncrementX;
vy+=(float)m_iIncrementY;
}
DebugFile.File << "Unlocking vertex buffer...";
hr=m_pVB->Unlock();
if (hr!=D3D_OK)
{
DebugFile.File << "FAILED" << endl;
} else DebugFile.File << "SUCCESS" << endl;
}
void CScreenGrid::Render(float fTimeDelta)
{
//m_pDevice->SetRenderState(D3DRS_CULLMODE,D3DCULL_NONE);
//m_pDevice->SetRenderState(D3DRS_FILLMODE,D3DFILL_WIREFRAME);
//Set stream source
m_pDevice->SetStreamSource(0,m_pVB,0,sizeof(TLVertex));
//Set FVF
m_pDevice->SetFVF(TLVertex::FVF);
m_pDevice->SetTexture(0,m_pTexture);
//Draw it
HRESULT hr=m_pDevice->DrawPrimitive(D3DPT_TRIANGLELIST,0,m_dwTotalTriangles);
if (hr!=D3D_OK)
{
DebugFile.File << "DrawPrimitive() - FAILED with D3DERR_INVALIDCALL";
}
//
//m_pDevice->SetRenderState(D3DRS_FILLMODE,D3DFILL_SOLID);
m_pDevice->SetTexture(0,NULL);
}
void CScreenGrid::Scroll(int irelScrollX,int irelScrollY)
{
m_iScrollX+=irelScrollX;
m_iScrollY+=irelScrollY;
if (m_iScrollX>=(m_iIncrementX-1))
{
irelScrollX=-m_iScrollX+irelScrollX;
m_iScrollX=0.0f;
}
if (m_iScrollX<=(-m_iIncrementX+1))
{
irelScrollX=-m_iScrollX+irelScrollX;
m_iScrollX=0.0f;
}
if (m_iScrollY>=m_iIncrementY)
{
irelScrollY=-m_iScrollY+irelScrollY;
m_iScrollY=0.0f;
}
if (m_iScrollY<=-m_iIncrementY)
{
irelScrollY=-m_iScrollY+irelScrollY;
m_iScrollY=0.0f;
}
TLVertex *Vertices;
m_pVB->Lock(0,0,(void **)&Vertices,0);
for (DWORD i=0;i<m_dwTotalVerts;i++)
{
Vertices[i].Pos.x+=(float)irelScrollX;
Vertices[i].Pos.y+=(float)irelScrollY;
}
m_pVB->Unlock();
}
void CScreenGrid::Rotate(float fRadians)
{
D3DXMATRIX rot;
D3DXMatrixRotationZ(&rot,fRadians);
TLVertex *Vertices;
m_pVB->Lock(0,0,(void **)&Vertices,0);
for (DWORD i=0;i<m_dwTotalVerts;i++)
{
D3DXVec3TransformCoord(&Vertices[i].Pos,&Vertices[i].Pos,&rot);
//Vertices[i].x+=(float)irelScrollX;
//Vertices[i].y+=(float)irelScrollY;
}
m_pVB->Unlock();
}
CScreenGrid.h
Code:
#ifndef CSCREENGRID
#define CSCREENGRID
#include "d3dx9.h"
#include "CFileLog.h"
#include "CTilemap.h"
#include <string>
struct TLVertex
{
D3DXVECTOR3 Pos;
float rhw;
D3DCOLOR diffuse;
float u,v;
static const DWORD FVF;
TLVertex(float _x,float _y,float _z,float _u,float _v):Pos(_x,_y,_z),rhw(1.0f),u(_u),v(_v) {}
TLVertex(void):Pos(0.0f,0.0f,0.0f),rhw(1.0f),u(0.0f),v(0.0f) {}
};
class CScreenGrid
{
protected:
CTilemap *TileMap;
IDirect3DTexture9 *m_pTexture;
IDirect3DDevice9 *m_pDevice;
IDirect3DVertexBuffer9 *m_pVB;
WORD m_iScreenHeight;
WORD m_iScreenWidth;
WORD m_iTileHeight;
WORD m_iTileWidth;
WORD m_iNumTilesVert;
WORD m_iNumTilesHoriz;
DWORD m_dwTotalTriangles;
DWORD m_dwTotalVerts;
DWORD m_dwTotalTiles;
int m_iScrollX;
int m_iScrollY;
int m_iIncrementX;
int m_iIncrementY;
DWORD m_dwScrollBoundsX;
DWORD m_dwScrollBoundsY;
DWORD m_dwWorldX;
DWORD m_dwWorldY;
//Debugging only
CFileLog DebugFile;
public:
CScreenGrid(void):m_pTexture(NULL),m_pDevice(NULL),m_iTileHeight(0),m_iTileWidth(0),
m_iScreenHeight(0),m_iScreenWidth(0),m_iIncrementX(0),m_iIncrementY(0),
m_dwTotalTriangles(0),m_dwTotalVerts(0),m_dwTotalTiles(0) {}
virtual ~CScreenGrid(void)
{
if (m_pTexture) m_pTexture->Release();
if (m_pVB) m_pVB->Release();
if (m_pSoftwareVerts) delete [] m_pSoftwareVerts;
m_pTexture=NULL;
m_pVB=NULL;
m_pSoftwareVerts=NULL;
DebugFile.Close();
}
void SetSize(int iTileW,int iTileH,int iScrW,int iScrH)
{
m_iTileWidth=iTileW;
m_iTileHeight=iTileH;
m_iScreenWidth=iScrW;
m_iScreenHeight=iScrH;
m_iIncrementX=m_iScreenWidth/m_iTileWidth;
m_iIncrementY=m_iScreenHeight/m_iTileHeight;
}
void SetNumTilesHV(int iNumHorz,int iNumVert,int iScrW,int iScrH)
{
m_iTileWidth=iScrW/iNumHorz;
m_iTileHeight=iScrH/iNumVert;
m_iScreenWidth=iScrW;
m_iScreenHeight=iScrH;
m_iIncrementX=m_iTileWidth;
m_iIncrementY=m_iTileHeight;
m_iNumTilesHoriz=iNumHorz+2;
m_iNumTilesVert=iNumVert+2;
}
void SetWorldPos(DWORD dwWorldX,DWORD dwWorldY)
{
m_dwWorldX=dwWorldX;
m_dwWorldY=dwWorldY;
}
void SetScrollBounds(DWORD dwScrollBoundsX,DWORD dwScrollBoundsY)
{
m_dwScrollBoundsX=dwScrollBoundsX;
m_dwScrollBoundsY=dwScrollBoundsY;
}
void Rotate(float fRadians);
void Create(IDirect3DDevice9 *Device,std::string File);
void Render(float fTimeDelta);
void Scroll(int irelScrollX,int irelScrollY);
};
#endif
To alter this for maps with more than 1 texture.
- Calculate the offset into the tile map data
- Retrieve the texture ID from the map
- Retrieve the texture from the texture manager using the ID
- If the current texture ID is not the ID in the map, use SetTexture() to change the texture.
In this code the grid simply just scrolls left and right, up and down. The only difference between this and a real time map is that as you scroll:
Code:
...
...
if (m_iScrollX % m_iTextureSizeX==0)
{
m_dwTileMapOffsetX++;
}
if (m_iScrollY % m_iTextureSizeY==0)
{
m_dwTileMapOffsetY++;
}
DWORD dwTileMapStartingOffset=m_dwTileMapOffsetY*m_dwTileMapWidth+m_dwTileMapOffsetY;
...
...
This will change the starting image at the upper left corner of the grid. If you change the starting image then in effect you have changed the entire map position.
For instance:
Look at this map: MAP1
1020000
1210303
0704050
1040305
Ok now scroll once to the right: MAP2
020000.
210303.
704050.
040305.
Notice the first column from MAP1 is not in MAP2
The periods indicate the edge of the map
See. The only difference between the two maps is that we start at a different offset on row 1. But the overall grid behind it all is still the same.
So if we had a cellsize of 1 or 1 pixel then this would work. However we must take into account each tile's size. So if each tile is 64 pixels then we can find out when to increment the starting map offsets for x and y by using modulo arithmetic.
My code is complex because I do not re-calculate the y*width+x inside of the loop. I calculate once and then increment. To move down one row, I simply add in width - which gives the same result as re-calculation y*width+x, but it doesn't require a multiply so it's faster. So the algo is huge because it is optimized.
One loop, couple of increments, couple of adds, and that's it. All linear.
The CTileMap pointer is not used as of yet, but it will simply be a pointer to the current map in memory. This map holds the IDs of the textures. Since know our current offset we simpy retrieve the ID of the tile at Map[offset] and then do TileManager->GetTexture(ID) to get the actual texture.
The example code I gave you does not do this as of yet. Feel free to change it. It's not far from being a fully functional per-pixel scrolling tile engine renderer.
The tile editor for this engine does do this. Here is the OnDraw() code from the editor.
Code:
void CZeldaEditorView::OnDraw(CDC* pDC)
{
CDC MemDC;
if (!MemDC.CreateCompatibleDC(GetDC()))
{
::MessageBox(0,"Failed to create memory DC",0,0);
}
CZeldaEditorDoc* pDoc = (CZeldaEditorDoc *)GetDocument();
CMainFrame *ptrFrame=(CMainFrame *)AfxGetMainWnd();
DWORD numMaps=pDoc->m_pMapManager->GetNumberOfMaps();
HDC dcDest=pDC->GetSafeHdc();
CBrush blackbrush;
blackbrush.CreateSolidBrush(RGB(0,0,0));
CRect ScreenRect;
GetClientRect(&ScreenRect);
pDC->FillRect(&ScreenRect,&blackbrush);
//MemDC.FillRect(&ScreenRect,&blackbrush);
CPoint mouse;
::GetCursorPos(&mouse);
if (pDoc->m_pTileManager && numMaps>1)
{
int TileSize=pDoc->m_iTileSize;
int iOffsetHoriz=abs(m_iScrollX/m_iGridSize);
int iOffsetVert=abs(m_iScrollY/m_iGridSize);
//int iStartTile=iOffsetVert*m_iMapSizeX+iOffsetVert;
//int iCurTile=iStartTile;
//ComputeStartGrid();
for (DWORD map=m_dwStartMapNum;map<m_dwEndMapNum;map++)
{
CMap *ptrMap=pDoc->m_pMapManager->GetMapClass(map);
//CMap *ptrMove=pDoc->m_pMapManager->GetMapClass(0);
int CurrentY=(-m_iScrollY/m_iGridSize);
int CurrentX=(-m_iScrollX/m_iGridSize);
int OriginX=CurrentX;
DWORD offset=CurrentY*pDoc->m_iMapSizeX+CurrentX;
int offsetx=m_iScrollX % m_iGridSize;
int offsety=m_iScrollY % m_iGridSize;
DWORD startoffset=offset;
CRect GridRect;
pDC->SetTextColor(RGB(255,0,0));
pDC->SetBkColor(0);
for (int i=offsety;i<ScreenRect.bottom;i+=m_iGridSize)
{
for (int j=offsetx;j<ScreenRect.right;j+=m_iGridSize)
{
DWORD ID=ptrMap->GetValueAtOffset(offset);
if (ID!=0xFFFFFFFF)
{
if (m_bViewIDs==false && m_bViewOffset==false && m_bViewRowCol==false)
{
CDC *tempDC=pDoc->m_pTileManager->GetTileDC(ID);
HDC dcSource=tempDC->GetSafeHdc();
UINT dwTransColor=pDoc->m_pTileManager->GetTransColor(ID);
::TransparentBlt(dcDest,
j,i,
m_iGridSize,m_iGridSize,
dcSource,
0,0,
TileSize,TileSize,
dwTransColor);
}
}
//Draw the grid
GridRect.left=j;
GridRect.top=i;
GridRect.right=j+m_iGridSize;
GridRect.bottom=i+m_iGridSize;
GridRect.NormalizeRect();
if (m_bGrid)
{
CBrush gridbrush;
gridbrush.CreateSolidBrush(RGB(255,255,255));
pDC->FrameRect(&GridRect,&gridbrush);
}
if (m_bViewIDs==true || m_bViewOffset==true || m_bViewRowCol==true)
{
int midx=GridRect.Width()>>1;
int midy=GridRect.Height()>>1;
CString GridText;
if (m_bViewIDs)
{
if (ID==0xFFFFFFFF)
{
GridText="-1";
} else GridText.Format("%u",ID);
}
if (m_bViewOffset) GridText.Format("%u",offset);
if (m_bViewRowCol) GridText.Format("%u/%u",CurrentY,CurrentX);
CSize size;
size=pDC->GetTextExtent(GridText);
int x=j+midx-(size.cx>>1);
int y=i+midy-(size.cy>>1);
if ( (x+size.cx) < (j+GridRect.Width()) )
{
pDC->TextOut(x,y,GridText);
}
}
offset++;
CurrentX++;
if (CurrentX>=pDoc->m_iMapSizeX) break;
}
CurrentX=OriginX;
startoffset+=pDoc->m_iMapSizeX;
offset=startoffset;
CurrentY++;
if (CurrentY>pDoc->m_iMapSizeY-1) break;
}
}
}
}
The editor code attempts to draw the map in the window. It bails and/or moves down one row if:
- The render for the current row reaches the right side of the window
- The render for the current row would be below the bottom of the window
- The current x offset into the map is out of range
- The current y offset into the map is out of range
Don't worry about all the DC crapola. It's just standard Windows garbage to get my bitmaps to display. I'm using some nifty tricks to make it display faster.
This is essentially the portion that retreives the texture based on the ID in the map.
Code:
...
CDC *tempDC=pDoc->m_pTileManager->GetTileDC(ID);
HDC dcSource=tempDC->GetSafeHdc();
UINT dwTransColor=pDoc->m_pTileManager->GetTransColor(ID);
::TransparentBlt(dcDest,
j,i,
m_iGridSize,m_iGridSize,
dcSource,
0,0,
TileSize,TileSize,
dwTransColor);
...
It took me a long time to get this figured out and working, but it works great.
It does use two for loops but either loop can bail at any time if those conditions I showed above are met so it renders quite fast.