Log back into Tank Brawl 2 – Armor Fury on Xbox now and join the battles against other players in PvP modes.
You will be able to team up locally on the same console to take on 5 other players over the internet in fast pace PvP battles. The battles are divided into short but fun rounds that occur over 6 different arenas including Japanese Garden, Lush Beach Island, Jungle Military Base, Harsh Snowy Mountain, Wrecked City Center and special driver only Colosseum.
You will be able to control the same familiar vehicles as featured in the main campaign such as the Giant Mecha, T90 main battle tank, TOS1A rocket system or the futuristic machine gun tank. All four human drivers are playable as well for added fun factor.
Occasionally, a big AI boss will be spawned and attack everyone, you will need to work together if you want to take him down. Powerful pick ups such as Air Bomber support and throwing bombs are also available.
Together with PvP mode, this update added many improvements to the main campaign such as you can now control a big battleship right at the start of the game, improved co-op experiences, improved gameplay hints, adding more leaderboards such as the campaign speedrun, and many more.
In Tank Brawl 2, we want tanks to leave track marks, jeeps leave tire marks on the ground. We looked at using the particle system ( include the ribbon module) to do it but it was difficult to make the track mark look continues and good. So we developed our own system where we would spawn polygons in real time as the track move.
To use this all you need to do is copy & paste UTrailMarkComponent’s cpp and h file into your game’s c++ project and updating it’s m_trailAlpha to turn on and off the track mark when required ( e.g when vehicle is in air, we don’t want the track mark visible ). You can also tweak m_numQuad, m_quadLength, m_quadWidth, m_textureLength to match your need as explained below.
The UTrailMarkComponent is attached to the tank’s track. When the component moves, we will update its custom vertex buffer in a way that will lay out the track mark on the ground like this:
UTrailMarkComponent derived from UMeshComponent, which allowing us to create and modify our own procedural mesh and render them on screen. As Unreal Engine only allowing update the whole vertex buffer one at a time ( no partial update ), The UTrailMarkComponent works by first create a full vertex buffer and index buffer of m_numQuad number of quads. Look inside UTrailMarkComponent::Initialise(int32 NumQuad) to see how we setup the vertex and index buffer. Initially all those vertices have its alpha values = 0, so nothing is visible. At first when the track mark is first visible, we update the first quad alpha to be 1, and put its first 2 vertices at the current track’s location and when UTrailMarkComponent moved to a new position, we update the 2 vertices at the end of the current quad to match the new position ( Look inside UTrailMarkComponent::calculateVertices for detail ). If the current quad’s length becomes longer m_quadLength, we will stop updating this quad and leave it unchanged and move to the next quad and continue this process. When we run out of quads to use, we go back to the begining and reuse the first quad ( so the very earliest track mark will disappear once the whole track mark becomes too long ).You can look at UTrailMarkComponent::updateTrail function to see this process in detail.
UTrailMarkComponent::calculateFirstVertices. This function calculate the position and uv of the first 2 vertices of the quad that start the trail ( i.e the first quad of the track mark or the first quad after vehicle is in air and land on ground ). The uv of those 2 vertices is 0, so the track texture start from begining when a new continues track trail starts.
UTrailMarkComponent::calculateVertices: Calculate position and uv for current’s quad last 2 vertices, the uv of these 2 will depends on the current distance from the track mark start.
Below are the the .h and .cpp file for the UTrailMarkComponent that can just copy & paste into your game’s c++ project and start using it straight away. All you need to do is updating it’s m_trailAlpha to turn on and off the track mark when required ( e.g when vehicle is in air, we don’t want the track mark visible ). A majority of the code in UTrailMarkComponent is to deal with setting up the dynamic vertex buffer, that we copied from Unreal Engine’s CustomMeshComponent: These includes:
class FCustomMeshSceneProxy : public FPrimitiveSceneProxy.
class FCustomMeshVertexFactory : public FLocalVertexFactory
class FCustomMeshIndexBuffer : public FIndexBuffer
class FCustomMeshVertexBuffer : public FVertexBuffer
TrailMarkComponent.h
#pragma once
#include "CoreMinimal.h"
#include "UObject/ObjectMacros.h"
#include "Interfaces/Interface_CollisionDataProvider.h"
#include "Components/MeshComponent.h"
#include "PhysicsEngine/ConvexElem.h"
#include "TrailMarkComponent.generated.h"
class FPrimitiveSceneProxy;
/**
* Struct used to specify a tangent vector for a vertex
* The Y tangent is computed from the cross product of the vertex normal (Tangent Z) and the TangentX member.
*/
USTRUCT(BlueprintType)
struct FTrailMarkTangent
{
GENERATED_USTRUCT_BODY()
/** Direction of X tangent for this vertex */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Tangent)
FVector TangentX;
FTrailMarkTangent()
: TangentX(1.f, 0.f, 0.f)
{}
FTrailMarkTangent(float X, float Y, float Z)
: TangentX(X, Y, Z)
{}
FTrailMarkTangent(FVector InTangentX, bool bInFlipTangentY)
: TangentX(InTangentX)
{}
};
/** One vertex for the mesh, used for storing data internally */
USTRUCT(BlueprintType)
struct FTrailMarkVertex
{
GENERATED_USTRUCT_BODY()
/** Vertex position */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Vertex)
FVector Position;
/** Vertex normal */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Vertex)
FVector Normal;
/** Vertex tangent */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Vertex)
FTrailMarkTangent Tangent;
/** Vertex color */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Vertex)
FColor Color;
/** Vertex texture co-ordinate */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Vertex)
FVector2D UV0;
FTrailMarkVertex()
: Position(0.f, 0.f, 0.f)
, Normal(0.f, 0.f, 1.f)
, Tangent(FVector(1.f, 0.f, 0.f), false)
, Color(255, 255, 255)
, UV0(0.f, 0.f)
{}
};
/**
* Component that allows you to specify custom triangle mesh geometry
* Beware! This feature is experimental and may be substantially changed in future releases.
*/
UCLASS(hidecategories = (Object, LOD), meta = (BlueprintSpawnableComponent), ClassGroup = Rendering)
class ARM_API UTrailMarkComponent : public UMeshComponent, public IInterface_CollisionDataProvider
{
GENERATED_UCLASS_BODY()
public:
virtual void TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction) override;
virtual FMatrix GetRenderMatrix() const override;
//~ Begin UPrimitiveComponent Interface.
virtual FPrimitiveSceneProxy* CreateSceneProxy() override;
//~ End UPrimitiveComponent Interface.
//~ Begin UMeshComponent Interface.
virtual int32 GetNumMaterials() const override;
//~ End UMeshComponent Interface.
//~ Begin UObject Interface
virtual void PostLoad() override;
//~ End UObject Interface.
UFUNCTION(BlueprintCallable, Category = "Arm", meta = (AutoCreateRefTerm = "Normals, UV0, VertexColors, Tangents"))
void UpdateMesh(int32 startIndex, const TArray& Vertices, const TArray& Normals, const TArray& UV0, const TArray& VertexColors, const TArray& Tangents, bool pushToRenderThread);
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Arm")
float m_numQuad = 20;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Arm")
float m_quadLength = 150;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Arm")
float m_quadWidth = 150;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Arm")
float m_textureLength = 1500;
float m_trailAlpha = 1.0f;
private:
void Initialise(int NumQuad);
void updateTrail(float DeltaTime, const FVector &pos, float alpha);
//~ Begin USceneComponent Interface.
virtual FBoxSphereBounds CalcBounds(const FTransform& LocalToWorld) const override;
//~ Begin USceneComponent Interface.
friend class FTrailMarkComponentSceneProxy;
/** Vertex buffer for this section */
UPROPERTY()
TArray ProcVertexBuffer;
/** Index buffer for this section */
UPROPERTY()
TArray ProcIndexBuffer;
/** Reset this section, clear all mesh info. */
void Reset()
{
ProcVertexBuffer.Empty();
ProcIndexBuffer.Empty();
}
void calculateFirstVertices(const FVector &pos, const FVector &dir, TArray& vertices, TArray& normals, TArray& uv0, TArray& vertexColors) const;
void calculateVertices(const FVector &pos, const FVector &lastPos, float dist, float alpha, TArray& vertices, TArray& normals, TArray& uv0, TArray& vertexColors) const;
// Trail update working
enum State
{
None,
First,
Working
};
State m_state = None;
float m_dist = 0;
FVector m_lastPos;
float m_alpha = 0;
int m_currentVertIndex = 0;
};
In Tank Brawl 2, player uses right stick to control the turret and left stick to control the tank movement. The result is that the turret rotation is controlled by code whereas all other component movements are control by animation assets such as shooting, braking, turning, accelerating
Turret join controlled by player right stick input
Notice how the tank recoils correctly regardless of which direction the turret is facing. We achieve this by setting up the whole tank as one skeletal mesh and its turret base have three joins: PreCode -> Code -> PostCode.
The Code joins rotation is completely controlled by code ( i.e player’s input right stick ), all other joins are controlled normally by animation assets as shown in this root anim-graph
How does this help ? Well, imagine if player turns the turret 90 degree right. Then we look at the following scenario:
– Tank Brake, Turret nudge forward relative to tank hull. Artist will need to animate this nudge movement on the PreCode Turret joins as we want the turret always nudge to the tank’s front regardless of its angle.
– Tank Shoot: The whole turret recoils back ward relative to where it’s facing. In this case the artist will put this recoil movement in the “PostCode” turret joins.
The State Machine
Inside the state machine is where we trigger all the animations.
The transition to Firing, Turning Left..etc state are triggered whenever the tank turn, shoot or brake and when the relevant animation finishes we return to the “Check” conduit to transition again to the appropriate state.
Fire Animation
Inside the Firing state, we play this animation graph:
To make the tank hull recoils correctly when cannon fire with turret angled at any direction, artist create four recoil animation for the hull: Recoil Forward, Recoil Back, Recoil Left and Recoil Right. When the tank fire we calculate the angle of the turret facing compare to tank’s hull forward direction and then blend between 2 of the 4 animations above. For example if the turret is facing 45 degree North-East we blend 50% of Front Recoil and 50% of Right Recoil and 0% all the other 2.
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.
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:
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:
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
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;
});
}
}
}