The BETA
For the Beta build there were a couple features that we really wanted to get working. For me the big thing was making sure that the collision data was building nicely so that the games collision would function, and this was indeed achieved which was great.
Worms also animate when they move around, walk and jump which is really cool to see. The weapon system in the game is also starting to take shape, hopefully we can fledge it out a bit more for the actual release. It’s a good start!
One of the things that I was working on just before the beta was a way to optimise and remove large chunks of repetitive code from my previously added subsystems.
Because of my queue approach to adding images with blend states worked very nicely I ended up using really similar methods for several different subsystems, and although this didn’t cause issues it did lead to a fair amount of repetition which I figured could be resolved.
The Terrain Change Queue
I decided the best way to condense and remove repetition was to compact these repeated methods into one class, the TerrainChangeQueue class.
Why I thought this was a good idea
Before I talk about the new method however I want to highlight what I mean when I say repetitive code.
Here is an extract of the terrain2d.h
public:
void addDeformationToTerrain(std::string _deformationFileName, ID3D11Device* _Device, Vector2 _pos);
void releaseDeformationQue();
std::queue<ImageGO2D*> getDeformationData();
void addStampToTerrain(std::string _stampFileName, ID3D11Device* _Device, Vector2 _pos);
void releaseStampQue();
std::queue<ImageGO2D*> getStampData();
private:
std::queue<ImageGO2D*> deformation_data;
std::queue<ImageGO2D*> stamp_data;
For every subsystem that wants to mess with the render target several times we need to use the queue method. And for every queue method we need a queue of images that will be applied to the render target. That means each queue will need a function to release the memory of them if needed, as well as a function to add a new image to the queue and the way to get the queue for processing.
If you look closely you’ll notice that the signature for the add function for both subsystems in the example are the same.
And that’s because they basically are:
void Terrain2D::addDeformationToTerrain(std::string _deformationFileName, ID3D11Device* _Device, Vector2 _pos)
{
ImageGO2D* deformation_texture = new ImageGO2D(_deformationFileName, _Device);
deformation_texture->SetPos(_pos);
deformation_data.push(deformation_texture);
}
void Terrain2D::addStampToTerrain(std::string _stampFileName, ID3D11Device* _Device, Vector2 _pos)
{
ImageGO2D* stamp_texture = new ImageGO2D(_stampFileName, _Device);
stamp_texture->SetPos(_pos);
stamp_data.push(stamp_texture);
}
In fact all 3 functions are almost identical.
This means making our class work with all existing subsystems nice and simple.
However there is one issue that could be a problem. Processing the data itself could be a challenge, but let’s quickly look at how I was doing that.
Here is an example for the deformation queue:
std::queue<ImageGO2D*> tdd_queue = m_terrainData->getDeformationData();
if (!tdd_queue.empty()) //If there is deformation to handle do the following:
{
m_terrain->Begin(m_d3dContext.Get());
m_d3dContext->OMSetBlendState(m_terrain->GetDigBlend(), 0, 0xffffff);
m_DD2D->m_Sprites->Begin(DirectX::SpriteSortMode_Deferred, m_terrain->GetDigBlend());
while (!tdd_queue.empty())
{
ImageGO2D* tdd_texture = tdd_queue.front();
if (m_terrainData->getPixelPerfectDeformationMode()) //Consider called swaping getPixelPerfectDeformationMode for perfect pixel accuracy.
tdd_texture->TerrainDrawTest(m_DD2D);
else
tdd_texture->Draw(m_DD2D);
m_debugLogger->writeLine(DebugLogger::TERRAIN, DebugLogger::MESSAGE,
("Drawn object using dig blend at: " + to_string(tdd_texture->GetPos().x) + "," + to_string(tdd_texture->GetPos().y) + " in " +
((m_terrainData->getPixelPerfectDeformationMode()) ? "Pixel perfect mode..." : "Camera mode...")));
tdd_queue.pop();
}
m_terrainData->releaseDeformationQue();
m_DD2D->m_Sprites->End();
m_terrain->End(m_d3dContext.Get());
m_terrainData->generateStartingSolids(m_terrain, m_d3dContext.Get(), m_GD); //This method remaps the **WHOLE** map, it takes about 0.04 seconds, so is fine for testing, but I will make this only remap what is needed later. Will work for now.
}
As you can see it checks if the queue is empty, because there is no point running the code below if it is, then as previously talked about it prepares the render target and blend states and loops through the queue applying each image.
Now let’s look at another processing example:
std::queue<ImageGO2D*> ts_queue = m_terrainData->getStampData();
if (!ts_queue.empty()) //If there are stamps to handle do the following:
{
m_terrain->Begin(m_d3dContext.Get());
m_d3dContext->OMSetBlendState(m_states->Opaque(), 0, 0xffffff);
m_DD2D->m_Sprites->Begin(DirectX::SpriteSortMode_Deferred, m_states->Opaque());
while (!ts_queue.empty())
{
ImageGO2D* ts_texture = ts_queue.front();
if (m_terrainData->getPixelPerfectDeformationMode())
ts_texture->TerrainDrawTest(m_DD2D);
else
ts_texture->Draw(m_DD2D);
m_debugLogger->writeMessage(DebugLogger::TERRAIN, "Drawn object using Stamp Blend at: " + to_string(ts_texture->GetPos().x) + "," + to_string(ts_texture->GetPos().y) + " in " +
((m_terrainData->getPixelPerfectDeformationMode()) ? "Pixel perfect mode..." : "Camera mode..."));
ts_queue.pop();
}
m_terrainData->releaseStampQue();
m_DD2D->m_Sprites->End();
m_terrain->End(m_d3dContext.Get());
}
This example shows the processing method for the stamp subsystem. And it’s clear that this is also similar to the last example. With care the processing of this could also be handled internally with our new class.
Okay so we know what is duplicated a bunch now so lets make the new class:
The Implementation
Okay so let’s check out its header file:
class TerrainChangeQueue
{
public:
TerrainChangeQueue(DebugLogger* _logger, ID3D11BlendState* _blend, std::string _name);
~TerrainChangeQueue();
void process(RenderTarget* _terrain, ID3D11DeviceContext* _device, DrawData2D* _DD2D, bool _drawMode);
bool isEmpty();
void addToQueue(ImageGO2D* _image);
void updateBlendstate(ID3D11BlendState* _blend);
vector<ImageGO2D*> returnTextureList();
private:
void releaseSelf();
std::string blend_name = "DEFAULT_VALUE";
std::queue<ImageGO2D*> data_que;
DebugLogger* debug_logger = nullptr;
ID3D11BlendState* blend_state = nullptr;
vector<ImageGO2D*> texture_list;
};
So firstly its constructor takes an instance of the logger and a name for this queue instance for debugging purposes as well as a blend state that should be used when applying this queue to the render target.
TerrainChangeQueue::TerrainChangeQueue(DebugLogger* _logger, ID3D11BlendState* _blend, std::string _name)
{
debug_logger = _logger;
blend_state = _blend;
blend_name = _name;
}
Then it’s deconstructor simply calls its private releaseSelf function which looks like this:
TerrainChangeQueue::~TerrainChangeQueue()
{
releaseSelf();
}
void TerrainChangeQueue::releaseSelf()
{
while (!data_que.empty())
{
ImageGO2D* element = data_que.front();
if (element)
{
delete element;
element = nullptr;
}
data_que.pop();
}
}
The releaseSelf function simply deletes each element from the queue.
Next up is a public function that just checks if the queue is empty, this could be used for debug purposes however is also used by the process function later.
bool TerrainChangeQueue::isEmpty()
{
return data_que.empty();
}
After that lets look at the addToQueue Function. This takes an ImageGO2D pointer. It simply adds images to the queue for later processing.
void TerrainChangeQueue::addToQueue(ImageGO2D* _image)
{
data_que.push(_image);
}
There is also a function for updating the blendstate if needed. I added this to avoid a cyclomatic error caused by the render target holding a blend state that I needed.
void TerrainChangeQueue::updateBlendstate(ID3D11BlendState* _blend)
{
blend_state = _blend;
}
We also have the ability to return the entire list of images as a vector if needed.
vector<ImageGO2D*> TerrainChangeQueue::returnTextureList()
{
return texture_list;
}
And last but not least we have the process function, let’s take a look at it in full:
void TerrainChangeQueue::process(RenderTarget* _terrain, ID3D11DeviceContext* _device, DrawData2D* _DD2D, bool _drawMode)
{
texture_list.clear();
if (!data_que.empty()) //If there is deformation to handle do the following:
{
_terrain->Begin(_device);
_device->OMSetBlendState(blend_state, 0, 0xffffff);
_DD2D->m_Sprites->Begin(DirectX::SpriteSortMode_Deferred, blend_state);
while (!data_que.empty())
{
ImageGO2D* tdd_texture = data_que.front();
texture_list.emplace_back(tdd_texture);
if (_drawMode)
tdd_texture->TerrainDrawTest(_DD2D);
else
tdd_texture->Draw(_DD2D);
debug_logger->writeMessage(DebugLogger::TERRAIN, ("Drawn object with "+ blend_name +" at: " + to_string(tdd_texture->GetPos().x) + "," + to_string(tdd_texture->GetPos().y) + " in " +
((_drawMode) ? "Pixel perfect mode..." : "Camera mode...")));
data_que.pop();
}
releaseSelf();
_DD2D->m_Sprites->End();
_terrain->End(_device);
}
}
As you can see it’s similar to the above process methods, but means we no longer require separate process methods per queue we try to make.
Now our processing in the game.cpp goes from this:
std::queue<ImageGO2D*> ts_queue = m_terrainData->getStampData();
if (!ts_queue.empty()) //If there are stamps to handle do the following:
{
m_terrain->Begin(m_d3dContext.Get());
m_d3dContext->OMSetBlendState(m_states->Opaque(), 0, 0xffffff);
m_DD2D->m_Sprites->Begin(DirectX::SpriteSortMode_Deferred, m_states->Opaque());
while (!ts_queue.empty())
{
ImageGO2D* ts_texture = ts_queue.front();
if (m_terrainData->getPixelPerfectDeformationMode())
ts_texture->TerrainDrawTest(m_DD2D);
else
ts_texture->Draw(m_DD2D);
m_debugLogger->writeMessage(DebugLogger::TERRAIN, "Drawn object using Stamp Blend at: " + to_string(ts_texture->GetPos().x) + "," + to_string(ts_texture->GetPos().y) + " in " +
((m_terrainData->getPixelPerfectDeformationMode()) ? "Pixel perfect mode..." : "Camera mode..."));
ts_queue.pop();
}
m_terrainData->releaseStampQue();
m_DD2D->m_Sprites->End();
m_terrain->End(m_d3dContext.Get());
}
to this:
m_terrainData->getStampData()->process(m_terrain, m_d3dContext.Get(), m_DD2D, m_terrainData->getPixelPerfectDeformationMode()); //Draws stamps to the RT
Which I think is quite an improvement.
To add a new TerrainChangeQueue all you need to do is create it in the constructor of the terrain data like so:
stamp_data = new TerrainChangeQueue(_logger, _states->Opaque(), "Stamp");
That is how the stamp TerrainChangeQueue is created.
Summery
Overall I am really happy with this change because it means I can shrink the code down significantly. It also means adding other subsystems that use queues is really easy now!
I’m happy I got it done in time for the beta and its extra debug visibility is quite helpful.