2 Replies - 974 Views - Last Post: 21 February 2016 - 07:38 AM Rate Topic: -----

#1 Blinkk   User is offline

  • D.I.C Head

Reputation: 2
  • View blog
  • Posts: 50
  • Joined: 07-May 15

Tiling in C++ using DirectX

Posted 19 February 2016 - 01:22 PM

Hey everyone, I'm currently working on a 2D dungeon-crawler in C++ using DirectX9 and a custom framework that I built (it's not even close to perfect but its functional) and I am having a tough time managing the tiles and how they get rendered to the screen.

Right now, I have all the tiles loaded in at the beginning of a level (with all other game objects). They are pulled in from a text file which determines which tile(s) from a sheet of tiles should be placed in each space, creating a map layout. The problem I'm running into is that when I render the tiles, it significantly slows down my game. I have a feeling it's because I am rendering each tile individually each frame, which seems very inefficient (even when only rendering those visible by the camera).

With that said, I am just seeking advice on how I should manage the tiles in my game. If anyone has suggestions, I'm open to it. Here is the code from the TileManager I currently have implemented. Please don't crucify it... I know it's broken.

Thank you in advance! :D/>

Tile.h:
#ifndef TILE_H
#define TILE_H
#include <iostream>
#include "stdafx.h"
#include "TwoDRenderer.h"
#include "IRenderable.h"
using namespace Smoke;

// Tile Size
#define TILE_SIZE_X 16
#define TILE_SIZE_Y 16

// Used for tiles that are 3 wide/long
enum TILEPOS{ FIRST = 0, SECOND, LAST };

namespace TileTypes
{
	const char SINGLE_PATH = '0';
	const char SINGLE_PATH_ALT = '1';
	const char SNGLE_PATH_CORNER = '2';
	const char SINGLE_WALL = '3';
	const char WIDE_WALL_1 = '4';
	const char WIDE_WALL_2 = '5';
	const char WIDE_WALL_3 = '6';
	const char LONG_WALL_1 = '7';
	const char LONG_WALL_2 = '8';
	const char LONG_WALL_3 = '9';
};

struct Tile : public IRenderable
{
public:
	Tile();
	virtual ~Tile();

	// Overridden Render()
	void Render() override;

	// Each tile has a renderer
	TwoDRenderer *renderer;

	// Each tile is either collidable or not
	bool isCollidable;

	// Flag to determine if an object is on a tile
	bool hasObject;
};


//////////////////
// TileType = '0'
//////////////////
struct SinglePathTile : public Tile
{
public:
	SinglePathTile(float posX, float posY, std::string textureToUse);
};


//////////////////
// TileType = '1'
//////////////////
struct SinglePathTileAlt : public Tile
{
public:
	SinglePathTileAlt(float posX, float posY, std::string textureToUse);
};


//////////////////
// TileType = '2'
//////////////////
struct CornerPathTile : public Tile
{
public:
	CornerPathTile(float posX, float posY, std::string textureToUse);
};


//////////////////
// TileType = '3'
//////////////////
struct SingleWallTile : public Tile
{
public:
	SingleWallTile(float posX, float posY, std::string textureToUse);
};



///////////////////////////
// TileType = '4', '5', '6'
///////////////////////////
struct WideWallTilePiece : public Tile
{
public:
	WideWallTilePiece(float posX, float posY, TILEPOS pos, std::string textureToUse);
};


////////////////////////////
// TileType = '7', '8', '9'
////////////////////////////
struct LongWallTilePiece : public Tile
{
public:
	LongWallTilePiece(float posX, float posY, TILEPOS pos, std::string textureToUse);
};

#endif


Tile.cpp:
#include "Tile.h"

////////////////
// Default tile
////////////////
Tile::Tile()
{
	isCollidable = false;
	hasObject = false;
	renderer = nullptr;
}

void Tile::Render()
{
	if (renderer->HasTexture())
		renderer->Render();
}

Tile::~Tile()
{
	// Nothing to deallocate
}


////////////////
// Path Tile (0)
////////////////
SinglePathTile::SinglePathTile(float posX, float posY, std::string textureToUse)
{
	renderer = new TwoDRenderer();
	if (!renderer)
	{
		debug << "Failed to create renderer for tile" << std::endl;
		return;
	}
	// Initialize Renderer
	renderer->Initialize(1.0f, 1.0f, TILE_SIZE_X, TILE_SIZE_Y, 8, 33, 33, 0, 0, 0.0f, posX, posY, textureToUse);

	isCollidable = false;
}


/////////////////////
// Alt Path Tile (1)
/////////////////////
SinglePathTileAlt::SinglePathTileAlt(float posX, float posY, std::string textureToUse)
{
	renderer = new TwoDRenderer();
	if (!renderer)
	{
		debug << "Failed to create renderer for tile" << std::endl;
		return;
	}
	// Initialize Renderer
	renderer->Initialize(1.0f, 1.0f, TILE_SIZE_X, TILE_SIZE_Y, 8, 32, 32, 0, 0, 0.0f, posX, posY, textureToUse);

	isCollidable = false;
}


///////////////////
// Corner Tile (2)
///////////////////
CornerPathTile::CornerPathTile(float posX, float posY, std::string textureToUse)
{
	renderer = new TwoDRenderer();
	if (!renderer)
	{
		debug << "Failed to create renderer for tile" << std::endl;
		return;
	}
	// Initialize Renderer
	renderer->Initialize(1.0f, 1.0f, TILE_SIZE_X, TILE_SIZE_Y, 8, 40, 40, 0, 0, 0.0f, posX, posY, textureToUse);

	isCollidable = false;
}


///////////////////
// Single Wall (3)
///////////////////
SingleWallTile::SingleWallTile(float posX, float posY, std::string textureToUse)
{
	renderer = new TwoDRenderer();
	if (!renderer)
	{
		debug << "Failed to create renderer for tile" << std::endl;
		return;
	}
	// Initialize Renderer
	renderer->Initialize(1.0f, 1.0f, TILE_SIZE_X, TILE_SIZE_Y, 8, 0, 0, 0, 0, 0.0f, posX, posY, textureToUse);

	isCollidable = true;
}


///////////////////////
// Wide Wall (4, 5, 6)
///////////////////////
WideWallTilePiece::WideWallTilePiece(float posX, float posY, TILEPOS pos, std::string textureToUse)
{
	renderer = new TwoDRenderer();
	if (!renderer)
	{
		debug << "Failed to create renderer for tile" << std::endl;
		return;
	}
	// Initialize Renderer
	switch (pos)
	{
	case FIRST:
		renderer->Initialize(1.0f, 1.0f, TILE_SIZE_X, TILE_SIZE_Y, 8, 1, 1, 0, 0, 0.0f, posX, posY, textureToUse);
		break;

	case SECOND:
		renderer->Initialize(1.0f, 1.0f, TILE_SIZE_X, TILE_SIZE_Y, 8, 2, 2, 0, 0, 0.0f, posX, posY, textureToUse);
		break;

	case LAST:
		renderer->Initialize(1.0f, 1.0f, TILE_SIZE_X, TILE_SIZE_Y, 8, 3, 3, 0, 0, 0.0f, posX, posY, textureToUse);
		break;
	}

	isCollidable = true;
}


///////////////////////
// Long Wall (7, 8, 9)
///////////////////////
LongWallTilePiece::LongWallTilePiece(float posX, float posY, TILEPOS pos, std::string textureToUse)
{
	renderer = new TwoDRenderer();
	if (!renderer)
	{
		debug << "Failed to create renderer for tile" << std::endl;
		return;
	}
	// Initialize Renderer
	switch (pos)
	{
	case FIRST:
		renderer->Initialize(1.0f, 1.0f, TILE_SIZE_X, TILE_SIZE_Y, 8, 8, 8, 0, 0, 0.0f, posX, posY, textureToUse);
		break;

	case SECOND:
		renderer->Initialize(1.0f, 1.0f, TILE_SIZE_X, TILE_SIZE_Y, 8, 16, 16, 0, 0, 0.0f, posX, posY, textureToUse);
		break;

	case LAST:
		renderer->Initialize(1.0f, 1.0f, TILE_SIZE_X, TILE_SIZE_Y, 8, 24, 24, 0, 0, 0.0f, posX, posY, textureToUse);
		break;
	}
	
	isCollidable = true;
}


TileManager.h:
#ifndef TILEMANAGER_H
#define TILEMANAGER_H
#include <iostream>
#include <fstream>
#include <map>
#include "Tile.h"
#include "stdafx.h"
using namespace Smoke;

// Source map size
#define SOURCE_MAP_X 128
#define SOURCE_MAP_Y 128

enum MAPS
{
	PROTOTYPE_MAP = 0,
	LEVEL_ONE_MAP
};

//////////////////////////
// Typedef a row of Tiles
// as a map of Tile*
//////////////////////////
typedef std::map<unsigned int, Tile*> TileRow;

class TileManager
{
private:
	// Map of maps of tiles
	std::map<unsigned int, TileRow> _tileMap;
	std::map<unsigned int, TileRow>::iterator _mIt;
	std::map<unsigned int, Tile*>::iterator _tIt;

	/*
		These variables will keep track of the number of rows
		and columns that the map should have based on the size of
		the game screen and the size of each tile (16x16)
	*/
	unsigned int _rows;
	unsigned int _columns;

	TileManager();

	/*
		These functions are used within the TileMap()
		function and are predefined to create a specific
		tiled map setup. New maps must be created manually,
		and their ID must be added to the MAPS enum.
	*/
	void Level_One();

	/*
		This function checks all surrounding tiles of a given
		tile position and determines whether or not those tiles 
		currently have an object or are collidable tiles
	*/
	void CheckSurroundingTiles(Vector2 tilePos, bool &up, bool &down, bool &left, bool &right);


public:
	static TileManager &GetInstance()
	{
		TileManager *pTemp = nullptr;

		if (!pTemp)
		{
			pTemp = new TileManager();
		}

		return (*pTemp);
	}

	~TileManager();

	/*
		This function takes in the name of a specific map as a 
		parameter and loads that map into the map surface

		Note: This should ONLY be called at the beginning
		of a new level and never within any updates
	*/
	void TileMap(unsigned int levelID);

	/*
		This function will simply draw the map to the screen

		Note: This should ONLY be called in the main Render()
	*/
	void DrawMap();

	/*
		Update function that gets the tile position of
		each active gameObject in the current level
	*/
	void Update(float deltaTime);

	/*
		This function is used to release all tile pointers

		Note: This should only be called at the end of a level (g_Manager->UnloadLevel())
	*/
	void PurgeMapObjects();

	/*
		Testing this function	
	*/
	void UpdatePlayerFlags();
};

#endif


TileManager.cpp
#include "TileManager.h"
#include "Engine.h"

TileManager::TileManager()
{
	// Get rows and columns
	_rows = SCREENH / TILE_SIZE_Y;
	_columns = SCREENW / TILE_SIZE_X;
}


TileManager::~TileManager()
{
	// Empty the map of maps of tiles
	if (!_tileMap.empty())
	{
		for (_mIt = _tileMap.begin(); _mIt != _tileMap.end();)/>/>/>/>
		{
			if (!(*_mIt).second.empty())
			{
				for (_tIt = (*_mIt).second.begin(); _tIt != (*_mIt).second.end();)/>/>/>/>
				{
					delete (*_tIt).second;
					_tIt = (*_mIt).second.erase(_tIt);
				}
				(*_mIt).second.clear();
			}
			_mIt = _tileMap.erase(_mIt);
		}
		_tileMap.clear();
	}
}


///////////////////////
// Map tiling function
///////////////////////
void TileManager::TileMap(unsigned int levelID)
{
	// Map files
	std::string textureFile = "";
	std::string layoutFile = "";

	// Determine which files to use
	switch (levelID)
	{
	case MAPS::PROTOTYPE_MAP:
		textureFile = "SourceMaps/purple_bricks.png";
		layoutFile = "./Textures/MapLayouts/Prototype_Level.txt";
		break;

	case MAPS::LEVEL_ONE_MAP:
		textureFile = "SourceMaps/hedge_maze.png";
		layoutFile = "./Textures/MapLayouts/Level_One.txt";
		break;

	default:
		debug << "\tFailed to find a map with ID = " << levelID << std::endl;
		break;
	}

	
	// Position variables
	float posX = 0.0f;
	float posY = 0.0f;

	// Stream variables
	std::ifstream inStream;
	char currentChar;

	// Open the layout file
	inStream.open(layoutFile);

	// Get each char and create a tile
	Tile* pTemp = nullptr;
	for (int r = 0; r < _rows; ++r)
	{
		for (int c = 0; c < _columns; ++c)
		{
			inStream >> currentChar;
			switch (currentChar)
			{
				// Single path
			case TileTypes::SINGLE_PATH:
				pTemp = new SinglePathTile(posX, posY, textureFile);
				if (_tileMap[r][c] == nullptr)
					_tileMap[r][c] = pTemp;
				posX += TILE_SIZE_X;
				break;

				// Alt Single path 
			case TileTypes::SINGLE_PATH_ALT:
				pTemp = new SinglePathTileAlt(posX, posY, textureFile);
				if (_tileMap[r][c] == nullptr)
					_tileMap[r][c] = pTemp;
				posX += TILE_SIZE_X;
				break;

				// Corner path
			case TileTypes::SNGLE_PATH_CORNER:
				pTemp = new CornerPathTile(posX, posY, textureFile);
				if (_tileMap[r][c] == nullptr)
					_tileMap[r][c] = pTemp;
				posX += TILE_SIZE_X;
				break;

				// Single wall
			case TileTypes::SINGLE_WALL:
				pTemp = new SingleWallTile(posX, posY, textureFile);
				if (_tileMap[r][c] == nullptr)
					_tileMap[r][c] = pTemp;
				posX += TILE_SIZE_X;
				break;


				////////////////
				// Wide Wall 
				////////////////
				// Piece 1
			case TileTypes::WIDE_WALL_1:
				pTemp = new WideWallTilePiece(posX, posY, TILEPOS::FIRST, textureFile);
				if (_tileMap[r][c] == nullptr)
					_tileMap[r][c] = pTemp;
				posX += TILE_SIZE_X;
				break;

				// Piece 2
			case TileTypes::WIDE_WALL_2:
				pTemp = new WideWallTilePiece(posX, posY, TILEPOS::SECOND, textureFile);
				if (_tileMap[r][c] == nullptr)
					_tileMap[r][c] = pTemp;
				posX += TILE_SIZE_X;
				break;

				// Piece 3
			case TileTypes::WIDE_WALL_3:
				pTemp = new WideWallTilePiece(posX, posY, TILEPOS::LAST, textureFile);
				if (_tileMap[r][c] == nullptr)
					_tileMap[r][c] = pTemp;
				posX += TILE_SIZE_X;
				break;

				////////////////
				// Long Wall 
				////////////////
				// Piece 1
			case TileTypes::LONG_WALL_1:
				pTemp = new LongWallTilePiece(posX, posY, TILEPOS::FIRST, textureFile);
				if (_tileMap[r][c] == nullptr)
					_tileMap[r][c] = pTemp;
				posX += TILE_SIZE_X;
				break;

				// Piece 2
			case TileTypes::LONG_WALL_2:
				pTemp = new LongWallTilePiece(posX, posY, TILEPOS::SECOND, textureFile);
				if (_tileMap[r][c] == nullptr)
					_tileMap[r][c] = pTemp;
				posX += TILE_SIZE_X;
				break;

				// Piece 3
			case TileTypes::LONG_WALL_3:
				pTemp = new LongWallTilePiece(posX, posY, TILEPOS::LAST, textureFile);
				if (_tileMap[r][c] == nullptr)
					_tileMap[r][c] = pTemp;
				posX += TILE_SIZE_X;
				break;

			default:
				posX += TILE_SIZE_X;
				break;
			}
		}

		// Increment position
		posY += TILE_SIZE_Y;
		posX = 0.0f;
	}

	inStream.close();
}


void TileManager::CheckSurroundingTiles(Vector2 tilePos, bool &up, bool &down, bool &left, bool &right)
{
	// Check left tile
	if (_tileMap[tilePos.GetY()][tilePos.GetX() - 1]->hasObject)
		left = true;
	else
		left = false;

	// Check right tile
	if (_tileMap[tilePos.GetY()][tilePos.GetX() + 1]->hasObject)
		right = true;
	else
		right = false;

	// Check up tile
	if (_tileMap[tilePos.GetY() - 1][tilePos.GetX()]->hasObject)
		up = true;
	else
		up = false;

	// Check down tile
	if (_tileMap[tilePos.GetY() + 1][tilePos.GetX()]->hasObject)
		down = true;
	else
		down = false;
}


///////////////////////
// Draw map function
///////////////////////
void TileManager::DrawMap()
{
	std::map<unsigned int, TileRow>::iterator i;
	std::map<unsigned int, Tile*>::iterator j;
	if (!_tileMap.empty())
	{
		// Get player position
		Vector2 playerPos = Vector2(0, 0);
		GraphicsComponent* pTempComp = nullptr;
		bool result = g_Engine->GetHandleManager()->GetAs(
			g_Engine->GetPlayer()->GetComponent(COMPONENTS::GRAPHICS_COMPONENT), pTempComp);
		if (result)
			playerPos = pTempComp->GetCurrentPos();

		unsigned int currentCol = playerPos.GetX() / TILE_SIZE_X;
		unsigned int currentRow = playerPos.GetY() / TILE_SIZE_Y;

		IRenderable* pTempTile = nullptr;
		for (int i = 0; i < 11; ++i)
		{
			for (int j = 0; j < 14; ++j)
			{
				pTempTile = dynamic_cast<IRenderable*>(_tileMap[currentRow + i][currentCol + j]);
				if (pTempTile)
					pTempTile->Render();

				pTempTile = dynamic_cast<IRenderable*>(_tileMap[currentRow + i][currentCol - j]);
				if (pTempTile)
					pTempTile->Render();

				pTempTile = dynamic_cast<IRenderable*>(_tileMap[currentRow - i][currentCol + j]);
				if (pTempTile)
					pTempTile->Render();

				pTempTile = dynamic_cast<IRenderable*>(_tileMap[currentRow - i][currentCol - j]);
				if (pTempTile)
					pTempTile->Render();
			}
		}

		//// Render all of the tiles
		//IRenderable *pTemp = nullptr;
		//for (i = _tileMap.begin(); i != _tileMap.end(); ++i)
		//{
		//	for (j = (*i).second.begin(); j != (*i).second.end(); ++j)
		//	{
		//		/*
		//			Attempt to cast each object into an IRenderableObject.
		//			This will ensure that it is a child of IRenderableObject
		//			and is therefore capable of calling Render()
		//		*/
		//		pTemp = dynamic_cast<IRenderable*>((*j).second);

		//		if (pTemp)
		//			pTemp->Render();
		//	}
		//}
	}
}


void TileManager::Update(float deltaTime)
{
	
}


void TileManager::PurgeMapObjects()
{
	// Empty the map of maps of tiles
	if (!_tileMap.empty())
	{
		for (_mIt = _tileMap.begin(); _mIt != _tileMap.end();)/>/>/>/>
		{
			if (!(*_mIt).second.empty())
			{
				for (_tIt = (*_mIt).second.begin(); _tIt != (*_mIt).second.end();)/>/>/>/>
				{
					delete (*_tIt).second;
					_tIt = (*_mIt).second.erase(_tIt);
				}
				(*_mIt).second.clear();
			}
			_mIt = _tileMap.erase(_mIt);
		}
		_tileMap.clear();
	}
}


void TileManager::UpdatePlayerFlags()
{
	if (!_tileMap.empty())
	{
		// Get player position
		Vector2 playerPos = Vector2(0, 0);
		GraphicsComponent* pTempComp = nullptr;
		bool result = g_Engine->GetHandleManager()->GetAs(
			g_Engine->GetPlayer()->GetComponent(COMPONENTS::GRAPHICS_COMPONENT), pTempComp);
		if (result)
			playerPos = pTempComp->GetCurrentPos();

		unsigned int currentCol = playerPos.GetX() / TILE_SIZE_X;
		unsigned int currentRow = playerPos.GetY() / TILE_SIZE_Y;

		////////////////////////////////
		// Check the surrounding tiles
		////////////////////////////////
		bool canGoLeft, canGoRight, canGoUp, canGoDown;

		// Check left tile
		if (_tileMap[currentRow][currentCol - 1]->isCollidable)
			canGoLeft = false;
		else
			canGoLeft = true;

		// Check right tile
		if (_tileMap[currentRow][currentCol + 1]->isCollidable)
			canGoRight = false;
		else
			canGoRight = true;

		// Check up tile
		if (_tileMap[currentRow - 1][currentCol]->isCollidable)
			canGoUp = false;
		else
			canGoUp = true;

		// Check down tile
		if (_tileMap[currentRow + 1][currentCol]->isCollidable)
			canGoDown = false;
		else
			canGoDown = true;


		/////////////////////////
		// Set player flags
		/////////////////////////
		// FIX THIS
		//pTemp->SetMovementFlags(canGoLeft, canGoRight, canGoUp, canGoDown);
	}
}



Is This A Good Question/Topic? 0
  • +

Replies To: Tiling in C++ using DirectX

#2 stayscrisp   User is offline

  • フカユ
  • member icon

Reputation: 1040
  • View blog
  • Posts: 4,325
  • Joined: 14-February 08

Re: Tiling in C++ using DirectX

Posted 21 February 2016 - 04:24 AM

Hi Blinkk,

It might be a good idea for you to start measuring some of your functions and try to nail down where the bottleneck is.

Get the time before you call your draw map function then get the time after, subtract after from before to get the time it took.
Was This Post Helpful? 0
  • +
  • -

#3 BBeck   User is offline

  • Here to help.
  • member icon


Reputation: 792
  • View blog
  • Posts: 1,886
  • Joined: 24-April 12

Re: Tiling in C++ using DirectX

Posted 21 February 2016 - 07:38 AM

If it's slow, you're probably not doing something right. Today's graphics cards can handle millions of triangles. Anything 2D should be a walk in the park for the graphics card.

You're probably right that it's the way you're doing the drawing. I'm far from an expert on "batching", but that's the first thing that comes to mind. I'm not even sure exactly what batching is, but I know XNA batches sprites.

What I do know however is that multiple draw calls will bring your frame rate to its knees. You need to get as much work out of a single draw call as possible. I'm not sure exactly what you are trying to do, but one idea that comes to mind is to draw all similar tiles together. So, if you have a grass tile, first of all you might be able to make that one quad and just repeat the texture at the proper interval. But say you have a monster sprite and you have 10 monsters. That can be drawn as a single mesh. The triangles in the mesh do not have to be connected when you submit them to the vertex buffer. You could even use a sprite sheet and have different sprites drawn on different quads using the UV coordinates. So, different ground tiles could all come from the same sprite sheet, which means one texture being stored in memory and not swapping textures in and out of memory, plus the entire ground could then be drawn as one mesh, even if every ground tile has a separate quad and every tile has a different graphic/sprite.

The thing that makes this different than a single quad for the ground is that by doing a separate quad for every tile, you can use different images on each tile based on UV coordinates within the sprite sheet. In theory, everything on screen could be drawn off one sprite sheet, or texture, and everything could be a single mesh with one draw call.

Another difference to point out is the difference between this and a 3D terrain. Here, the quads are not connected. Their corners do not share vertices even if they overlap. That allows you to use the UV coordinates to pull entirely different sprites off the sprite sheet. Whereas if they shared vertices, you could only repeat the same image across the surface. It means a lot more vertices, but the graphics card can handle millions. And it's far better than separate draw calls for each quad.

The important thing is you're reducing it to one draw call or at least a lot fewer draw calls than if you did a draw call for each tile. Draw calls are super expensive and anything that changes state, like changing the rasterizer is super expensive. I think you can create some of those states and switch pre-created states. I haven't played with that enough to really know. Most of what I've done has involved only one state for the graphics card.

Another thought is to compose your own sprite sheet at run time. I've never tried this and I'm not sure how you would do it. But it makes sense in my mind that if you have 100 different sprite sheets and you only need a couple of sprites off each sheet, you might be able to form a new texture in memory composed of the various sprites from different sheets. That way you can avoid storing 100 different sprite sheets that are 95% wasted space and take the 5% from each sheet and compile them into one image. Then you could store just that one image in memory and use it to draw the entire screen as one single mesh. I'm not sure that you can use multiple textures in a single draw call unless your shader is setup for using multiple textures. And then if it is, you probably have a very finite number of textures you can use. So, getting it all together on one texture that you can send to the shader may be critical.

I never do 2D stuff, so I haven't really worked through these problems. In 3D, I'm more thinking one texture and one draw call per model.

I would still think that 1,000 draw calls per frame would be in the capabilities of the graphics card. So, make sure your loops are pretty tight and you're not doing anything super expensive in the loop that can be done outside of the loop.

Anyway, hope that helps a little.

This post has been edited by BBeck: 21 February 2016 - 07:49 AM

Was This Post Helpful? 0
  • +
  • -

Page 1 of 1