Tank Brawl 2 is a shoot-up tank game with vehicle/driver class combination inspired by Titan Quest. You can drive jeeps, tanks, giant mechs, mobile rocket launcher in 3v3 Multiplayer and a campaign with 100 wows moment. It will be the best armor shooter ever.

Post tutorial Report RSS Unreal Engine destructible skeletal mesh

Tanks in Tank Brawl 2 use skeletal meshes with shooting, moving, braking ..etc animations to make them good. When shooting at those tanks, part of the skeletal mesh fall off and the rest still animate correctly.

Posted by on - Intermediate Client Side Coding

Unreal Engine doesn’t support this natively so after experimenting with many different techniques we finally settle with using a dynamic texture system which is fast and relatively simple to implement. In this technique, we make the skeletal mesh use a material that have opacity channel controlled by a 10×10 grey scale dynamic texture, so we can turn off part of it when a piece is chucked off, we also need to create a separate actor with a static mesh component of that part at same location to replace turned off part of the skeletal mesh but since it is a separate actor, it can be physically simulated ( fall and bounce on ground ). Below is the part the skeletal mesh’s material showing its Opacity setup:

DestructibleSkelMaterial

You can see that the texture is a Param2D type which when the game run, we will replace the default greyscale white texture tenTenWhite.png it with a dynamic 10×10 grey scale texture that we will create in code. This dynamic texture is looked up by a separate UV channel ( Shown here as TexCoord(1) instead of TexCoord(0)) of the skeletal mesh. When creating the skeletal mesh asset in 3D modelling model, you need to setup this UV1 channel so that each individual part of the mesh is mapped into a single cell inside the 10×10 grid like below:

Uv

Here you can see that the whole skeletal mesh have 36 parts, each have uv setup so each is whole inside a cell within a 10×10 grid, so they can be individually turn off/on by changing the alpha value of the corresponding pixel inside the 10×10 dynamic texture.

Initially the dynamic texture is all white which means the skeletal mesh will be rendered with all the part fully visible. When we want to make a part fall off, we do 2 things:

– Modify the dynamic texture so that the pixel at the part location (inside the UV map above ) change from 255 to 0. This will make the part disappeared from the skeletal mesh.

– Create a separate physic simulated actor with a static mesh represent the fall off part at the exact location that match its previous location in the skeletal mesh. This make the part appear to fall off and bouncing on the ground. So in addition to the 3D model for the whole tank ( skeletal mesh ) you also need one model (static mesh) for each part that can be fall off.

Below are c++ code for turning off parts of the skeletal mesh

TankActor.h

class ATankActor
{
	static const int m_textureSize = 10;

	UPROPERTY()
	UTexture2D* m_dynamicPartMaskTexture;

	UPROPERTY()
	UMaterialInstanceDynamic *m_dynamicMaterialInstance;

	FUpdateTextureRegion2D m_wholeTextureReagion;
	uint8 m_pixelArray[m_textureSize * m_textureSize] = { 255 };
}

Code TankActor.cpp

In our system, each part is identified by an index, and our array m_cluster->m_parts[index].m_indexIntoPixelArray tell us which index UV location ( the cell ) the part locate inside the 10×10 dynamic texture. The m_indexIndoPixelArray is just computed by U*10 + V . Which you need to setup according to your need.

void ATank::turnOffPart(int index, class USkeletalMeshComponent* skelMesh)
{	
	// Create the dynamic texture if it wasn't created
	if (!m_dynamicPartMaskTexture)
	{
		first = true;
		m_dynamicPartMaskTexture = UTexture2D::CreateTransient(m_textureSize, m_textureSize, PF_G8);
		m_dynamicPartMaskTexture->CompressionSettings = TextureCompressionSettings::TC_Grayscale;
		m_dynamicPartMaskTexture->SRGB = 0;
		m_dynamicPartMaskTexture->UpdateResource();
		m_dynamicPartMaskTexture->Filter = TF_Nearest;

		for (int i = 0; i < m_textureSize * m_textureSize; ++i) m_pixelArray[i]= 255; } // Create the dynamic material instance if it wasn't created and // aslo set the material to use the dynamic texture created above if (!m_dynamicMaterialInstance) { m_dynamicMaterialInstance = skelMesh->CreateAndSetMaterialInstanceDynamic(1);
		m_dynamicMaterialInstance->SetTextureParameterValue("componentMaskTxt", m_dynamicPartMaskTexture);

	}

	// Mark the pixel to be invisible
	m_pixelArray[m_cluster->m_parts[index].m_indexIntoPixelArray] = 0;

	// Copy data over to the dynamic texture
	UpdateTextureRegions(m_dynamicPartMaskTexture, 0, 1, &m_wholeTextureReagion, m_textureSize, 1, m_pixelArray, false);
}

namespace
{
	void UpdateTextureRegions(UTexture2D* Texture, int32 MipIndex, uint32 NumRegions, FUpdateTextureRegion2D* Regions, uint32 SrcPitch, uint32 SrcBpp, uint8* SrcData, bool bFreeData)
	{
		if (Texture->Resource)
		{
			struct FUpdateTextureRegionsData
			{
				FTexture2DResource* Texture2DResource;
				int32 MipIndex;
				uint32 NumRegions;
				FUpdateTextureRegion2D* Regions;
				uint32 SrcPitch;
				uint32 SrcBpp;
				uint8* SrcData;
			};

			FUpdateTextureRegionsData* RegionData = new FUpdateTextureRegionsData;

			RegionData->Texture2DResource = (FTexture2DResource*)Texture->Resource;
			RegionData->MipIndex = MipIndex;
			RegionData->NumRegions = NumRegions;
			RegionData->Regions = Regions;
			RegionData->SrcPitch = SrcPitch;
			RegionData->SrcBpp = SrcBpp;
			RegionData->SrcData = SrcData;

			ENQUEUE_UNIQUE_RENDER_COMMAND_TWOPARAMETER(
				UpdateTextureRegionsData,
				FUpdateTextureRegionsData*, RegionData, RegionData,
				bool, bFreeData, bFreeData,
				{
					for (uint32 RegionIndex = 0; RegionIndex < RegionData->NumRegions; ++RegionIndex)
					{
						int32 CurrentFirstMip = RegionData->Texture2DResource->GetCurrentFirstMip();
						if (RegionData->MipIndex >= CurrentFirstMip)
						{
							RHIUpdateTexture2D(
								RegionData->Texture2DResource->GetTexture2DRHI(),
								RegionData->MipIndex - CurrentFirstMip,
								RegionData->Regions[RegionIndex],
								RegionData->SrcPitch,
								RegionData->SrcData
								+ RegionData->Regions[RegionIndex].SrcY * RegionData->SrcPitch
								+ RegionData->Regions[RegionIndex].SrcX * RegionData->SrcBpp
							);
						}
					}
			if (bFreeData)
			{
				FMemory::Free(RegionData->Regions);
				FMemory::Free(RegionData->SrcData);
			}
			delete RegionData;
				});
		}
	}
}
Comments
wilsonC
wilsonC

This is pretty clever! Just wondering though, why didn't you attach the armor bits as components that you could just activate physics for to knock off?

Reply Good karma Bad karma+1 vote
PhungGames Author
PhungGames

Your suggestion works as well however the main reasons we did it like this are:
- Performance: I think using 1 skeletal mesh is faster than creating a dozen of individual components.
- Take more time to setup the blueprint ( i.e you need attach all the individual components to the right skeletal mesh's join, artist need to export all the individual components ) . Wheras, this way you only deal with one skeletal mesh. Our artist also says that doing UV is easier for him than keeping tracks of all the components.

Reply Good karma+2 votes
wilsonC
wilsonC

Interesting. It sounds like it's faster this way and involves less work. Thanks for sharing your workflow!

Reply Good karma Bad karma+1 vote
Post a comment
Sign in or join with:

Only registered members can share their thoughts. So come on! Join the community today (totally free - or sign in with your social account on the right) and join in the conversation.

Follow Report Profile
Icon
Tank Brawl 2
Platforms
Windows, XONE
Developer & Publisher
Phung Games
Contact
Send Message
Homepage
Tankbrawl2.com
Release date
Game watch
Follow
Related Games
Tank Brawl 2
Tank Brawl 2 Tactical Shooter
Related Engines
Unreal Engine 4
Unreal Engine 4 Commercial
Related Groups
Phung Games
Phung Games Developer & Publisher
Unreal Engine 4 Games
Unreal Engine 4 Games Fans & Clans with 688 members