Making vehicles for VCTF4

This page serves as a guideline for porting vehicles to VCTF4. Texture editing, UnrealEd and a UnrealScript knowledge (how vehicle code works) are required. For the code part, you can use the VCTF4 source code (available on the downloads page) as an additional reference.

VCTF4 includes VCTF4 versions of all stock UT2004 vehicles and some extra ones. However, there are plenty of custom vehicles out there on several VCTF and ONS maps and also standalone. Unfortunately, they will have problems in VCTF4:

To fix these issues, we need to
  1. Create the necessary skins: green, gold, and optionally neutral (a good developer will always provide a neutral skin as well)
  2. Subclass the vehicle and add the lacking functionality.
  3. Subclass or add any additionally required classes (e.g. side turrets, emitters, etc).

I will write this guideline with an example, which makes it a lot easier to follow. In this example I convert the Minigun Scorpion, which you can find on VCTF-ModernGulch, to a working VCTF4 version.

v-01

1 – Creating skins

This is not going to be a Photoshop tutorial or anything, I expect that whoever reads this knows how to use PS (or any other software of choice that supports 32-bit TGA export at least) and how to do what I will describe. I would like to share the method I used to create the skins for the VCTF4 vehicles.

It can basically be divided into the following steps:

  1. Create a base skin without colors and separate the colored parts into a new texture.
  2. Create 4 versions of the colored texture.
  3. Create a Combiner and a Shader to create the fully colored skins.
This method was most likely used to create the non-ECE vehicles for Onslaught. If you have a look at how the Scorpion skin is made up, for instance, you will see that there is a colorless base skin and 2 versions (tan and greenish) of the colored areas. This method has three advantages: In my example of creating 4 team skins for the Minigun Scorpion we will address the worst case in which the colored part is not yet split from the base part (e.g. ECE vehicles).

1.1 – Creating the base skin

First of all, turn on some music. This is the most time consuming part. Trust me, I can tell after doing this for the Paladin, SPMA and Cicada...

Assuming we already have a skin for our vehicle (the choice of whether to use Red or Blue is up to you), we open that in our image editing software:

v-02

Create two additional layers. One of these layers is going to contain the parts we want to colorize, the other one should contain special symbols such as warning signs or other things the color of which should remain constant. Do this by selecting all the parts that should be colorized copy them to the color mask layer. Then select all the parts that should remain unchanged and copy (or cut/paste) them to the constant layer. The three layers I created look like this:

v-03

This took a while, eh? From here on it gets easier.

Make the base skin greyscale. This will make the result look as cool as my ECE vehicle conversions - and you gotta admit they KICK ASS! After that, copy the contents of our constant layer back to this base skin. The result should look like this:

v-04

Save this texture (without the color layer) and you're done with the base skin as well as the neutral skin!

1.2 – Creating the color masks

Now, create a new texture with a black background and put the color mask layer on it. Reduce the texture's size to something like 256x256. We don't need so much detail here because we only add color information, the result will look just fine (recall my awesome ECE conversions). Greyscale the color layer to make sure the base color you edit from doesn't get involved later.

v-05

At this point I can only explain how I do this in Photoshop. Those who use a different software will have to find an equivalent way to do this:

  1. Create a layer effect for the texture
  2. Select the Color Overlay bit and choose a strong red (255, 0, 0)
  3. Set the draw type to Color Burn
This will result in the following:

v-06

We have just created ourselves a red color overlay texture. Save this one and repeat the process for the other team colors. I recommend the following RGB values for the individual team colors:

When all color textures are done, we will switch to UnrealEd.

1.3 – Finishing the skins in UnrealEd

Import all the textures you created into UnrealEd into a new texture package. I'll call mine MiniScorpTex.utx. Remember to compress them as DXT1 after importing, that will make the textures a lot smaller and the quality loss is bearable for use in UT2004. Don't forget to set the LODSet of the textures to LODSET_PlayerSkin

v-08

Now, repeat the following steps for all 4 skins:

  1. Create a new Combiner.
  2. Set the CombineOperation to CO_Add
  3. Set the AlphaOperation to AO_UseMask
  4. Set the Material1 to the color mask
  5. Set the Material2 to the colorless skin
  6. Set the Mask to the color mask
v-08-2
Voilą, we're done creating skins. Save the texture package, you (or your coder) will need it.

2 – Coding

We get to the coding part of the tutorial. I expect you to know how to set up and compile a UT2004 package and to know UnrealScript enough to understand what I will be doing.

2.1 – Get the source code

Assuming you are converting an existing vehicle like I do in my Minigun Scorpion example, you first should acquire its source code unless you want to recode it yourself (which is probably the better way from a copyright point of view, but that's not the matter at hand).

You can also choose an alternative way if the vehicle you are porting comes with a standalone package (ie in a .u file). Simply add that standalone package to your EditPackages and create a subclass of the vehicle. Note that your VCTF4 package will then require that original package to work.

2.2 – Setting up the project

By "project" I refer to a UT2004 package that is compiled from source code. Create your package directory in the UT2004 dir, add a Classes dir, put in the sources, etc. Copy your previously saved texture package into the package directory as well.

Note that you will need to add OLTeamVehicleGames to your EditPackages.

2.3 – Getting to work

Locate and open the vehicle's main source code file and add the following pieces of code to the file (or create a subclass with the following code added):

#EXEC OBJ LOAD FILE=MiniScorpTex.utx PACKAGE=MiniScorp

var() Material TeamSkins[4];
var() Material NeutralSkin;
var() Material NewSpawnOverlay[4];

//Create VCTF4 tinted headlights
simulated function PostNetBeginPlay()
{
	local int i;
	local bool bUseHeadlights;
	
	if(Level.NetMode != NM_DedicatedServer && Level.bUseHeadlights && !(Level.bDropDetail || (Level.DetailMode == DM_Low)))
	{
		bUseHeadlights = true;
		Level.bUseHeadlights = false; //HACK - we are going to create these ourselves
	}
	
	Super.PostNetBeginPlay();
	
	if(bUseHeadlights)
	{
		Level.bUseHeadlights = true; //set back
		HeadlightCorona.Length = HeadlightCoronaOffset.Length;

		for(i=0; i<HeadlightCoronaOffset.Length; i++)
		{
			HeadlightCorona[i] = spawn( class'OLHeadlightCorona', self,, Location + (HeadlightCoronaOffset[i] >> Rotation) );
			HeadlightCorona[i].SetBase(self);
			HeadlightCorona[i].SetRelativeRotation(rot(0,0,0));
			HeadlightCorona[i].Skins[0] = HeadlightCoronaMaterial;
			HeadlightCorona[i].ChangeTeamTint(Team);
			HeadlightCorona[i].MaxCoronaSize = HeadlightCoronaMaxSize * Level.HeadlightScaling;
		}

		if(HeadlightProjectorMaterial != None && Level.DetailMode == DM_SuperHigh)
		{
			HeadlightProjector = spawn( class'ONSHeadlightProjector', self,, Location + (HeadlightProjectorOffset >> Rotation) );
			HeadlightProjector.SetBase(self);
			HeadlightProjector.SetRelativeRotation( HeadlightProjectorRotation );
			HeadlightProjector.ProjTexture = HeadlightProjectorMaterial;
			HeadlightProjector.SetDrawScale(HeadlightProjectorScale);
			HeadlightProjector.CullDistance	= ShadowCullDistance;
		}
	}
}

//Attach all flags to the vehicle.
event KDriverEnter(Pawn P)
{
	local int i;
	local OLTeamsPlayerReplicationInfo PRI;

	Super.KDriverEnter(P);
	
	PRI = OLTeamsPlayerReplicationInfo(PlayerReplicationInfo);
	if(PRI != None)
	{
		for(i = 0; i < PRI.HasFlags.Length; i++)
			AttachFlag(PRI.HasFlags[i]);
	}
}

//Return all flags to the former driver.
event bool KDriverLeave(bool bForceLeave)
{
	local int i;
	local OLTeamsPlayerReplicationInfo PRI;
	local Pawn FormerDriver;
	local bool bResult;

	FormerDriver = Driver;
	bResult = Super.KDriverLeave(bForceLeave);
	
	if(FormerDriver != None)
	{
		PRI = OLTeamsPlayerReplicationInfo(FormerDriver.PlayerReplicationInfo);
		if(PRI != None)
		{
			for(i = 0; i < PRI.HasFlags.Length; i++)
				FormerDriver.HoldFlag(PRI.HasFlags[i]);
		}
	}
	
	return bResult;
}

//Attach a flag with a random rotation adjust, so one can see that you're carrying multiple flags.
function AttachFlag(Actor FlagActor)
{
    local rotator MoreGameObjRot;

	if(!bDriverHoldsFlag && (FlagActor != None))
	{
        MoreGameObjRot.Roll = -1 * rand(3500);
        MoreGameObjRot.Pitch = -1 * rand(4000);
	
		AttachToBone(FlagActor,FlagBone);
		FlagActor.SetRelativeRotation(FlagRotation + MoreGameObjRot);
		FlagActor.SetRelativeLocation(FlagOffset);
	}
}

//Set the correct skin depending on the team (or neutral).
simulated event TeamChanged()
{
	local int i;

	Super(Vehicle).TeamChanged();

	if(Team >= 0 && Team < 4)
		Skins[0] = TeamSkins[Team];
	else if(Team == 255)
		Skins[0] = NeutralSkin;

	if(Level.NetMode != NM_DedicatedServer && Team >= 0 && Team < 4)
		SetOverlayMaterial(NewSpawnOverlay[Team], 1.5, True);

	for(i = 0; i < Weapons.Length; i++)
		Weapons[i].SetTeam(Team);

	if (Level.NetMode != NM_DedicatedServer)
	{
		for(i = 0; i < HeadlightCorona.Length; i++)
			HeadlightCorona[i].ChangeTeamTint(Team);
	}
}

defaultproperties
{
	NewSpawnOverlay(0)=Material'XGameShaders.PlayerShaders.VehicleSpawnShaderRed'
	NewSpawnOverlay(1)=Material'XGameShaders.PlayerShaders.VehicleSpawnShaderBlue'
	NewSpawnOverlay(2)=Material'OLTeamGamesTex.Weapons.VehicleSpawnShaderGreen'
	NewSpawnOverlay(3)=Material'OLTeamGamesTex.Weapons.VehicleSpawnShaderGold'
	TeamSkins(0)=Shader'MiniScorp.RedCombiner'
	TeamSkins(1)=Shader'MiniScorp.BlueCombiner'
	TeamSkins(2)=Shader'MiniScorp.GreenCombiner'
	TeamSkins(3)=Shader'MiniScorp.GoldCombiner'
	NeutralSkin=Texture'MiniScorp.BaseTexture'
}
Adapt the #EXEC directive and the defaultproperties block to your texture names.

2.4 – Secondary seats

If your vehicle has any secondary seats (subclasses of ONSWeaponPawn, e.g. the HellBender Rear Gun), you'll need to modify (or subclass) them and register them in the vehicle base code. Add the following code:

//Attach all flags to the base vehicle (AttachFlag takes care of that).
event KDriverEnter(Pawn P)
{
	local int i;
	local OLTeamsPlayerReplicationInfo PRI;

	Super.KDriverEnter(P);
	
	PRI = OLTeamsPlayerReplicationInfo(PlayerReplicationInfo);
	if(PRI != None)
	{
		for(i = 0; i < PRI.HasFlags.Length; i++)
			AttachFlag(PRI.HasFlags[i]);
	}
}

//Return all flags to the former driver.
event bool KDriverLeave(bool bForceLeave)
{
	local int i;
	local OLTeamsPlayerReplicationInfo PRI;
	local Pawn FormerDriver;
	local bool bResult;

	FormerDriver = Driver;
	bResult = Super.KDriverLeave(bForceLeave);
	
	if(FormerDriver != None)
	{
		PRI = OLTeamsPlayerReplicationInfo(FormerDriver.PlayerReplicationInfo);
		if(PRI != None)
		{
			for(i = 0; i < PRI.HasFlags.Length; i++)
				FormerDriver.HoldFlag(PRI.HasFlags[i]);
		}
	}
	
	return bResult;
}

2.5 – Turrets

If your vehicle comes with any additional turrets (subclasses of ONSWeapon, e.g. Tank Cannon, HellBender Rear Gun), you'll need to modify (or subclass) them and register them in the vehicle base or the respective WeaponPawn code. Add the following code:

#EXEC OBJ LOAD FILE=MiniScorpTex.utx PACKAGE=MiniScorp

var() Material TeamSkins[4];
var() Material NeutralSkin;
var() class<Emitter> TeamFlashEmitterClass[4];

var bool bEffectsInitialized;

simulated function InitEffects()
{
    if (Level.NetMode == NM_DedicatedServer)
		return;
	
	bEffectsInitialized = true;
	
    if (FlashEmitter == None)
    {
		if(Team >= 0 && Team < 4 && TeamFlashEmitterClass[Team] != None)
			FlashEmitter = Spawn(TeamFlashEmitterClass[Team]);
		else
			FlashEmitter = Spawn(FlashEmitterClass);

		if(FlashEmitter != None)
		{
			FlashEmitter.SetDrawScale(DrawScale);
			if (WeaponFireAttachmentBone == '')
				FlashEmitter.SetBase(self);
			else
				AttachToBone(FlashEmitter, WeaponFireAttachmentBone);

			FlashEmitter.SetRelativeLocation(WeaponFireOffset * vect(1,0,0));
		}
    }

    if (AmbientEffectEmitterClass != none && AmbientEffectEmitter == None)
    {
        AmbientEffectEmitter = spawn(AmbientEffectEmitterClass, self,, WeaponFireLocation, WeaponFireRotation);
        if (WeaponFireAttachmentBone == '')
            AmbientEffectEmitter.SetBase(self);
        else
            AttachToBone(AmbientEffectEmitter, WeaponFireAttachmentBone);

        AmbientEffectEmitter.SetRelativeLocation(WeaponFireOffset * vect(1,0,0));
    }
}

simulated function SetTeam(byte T)
{
    Team = T;
    if(T >= 0 && T < 4)
        Skins[0] = TeamSkins[T];
	else if(T == 255)
		Skins[0] = NeutralSkin;
	else
		Super.SetTeam(T);
	
	RepSkin = Skins[0];
	
	if(bEffectsInitialized && T >= 0 && T < 4 && TeamFlashEmitterClass[T] != None)
	{
		//re-initialize effects
		DestroyEffects();
		InitEffects();
	}
}


defaultproperties
{
	TeamFlashEmitterClass(0)=None
	TeamFlashEmitterClass(1)=None
	TeamFlashEmitterClass(2)=None
	TeamFlashEmitterClass(3)=None
	TeamSkins(0)=Shader'MiniScorp.RedCombiner'
	TeamSkins(1)=Shader'MiniScorp.BlueCombiner'
	TeamSkins(2)=Shader'MiniScorp.GreenCombiner'
	TeamSkins(3)=Shader'MiniScorp.GoldCombiner'
	NeutralSkin=Texture'MiniScorp.BaseTexture'
}
Adapt the #EXEC directive and the defaultproperties block to your texture names. If the original weapon uses flash emitters, subclass them so their colors match the vehicle's colors, then register them in the TeamFlashEmitterClass array.

Depending on what you are converting you might have to add some things to the effect code. Concerning side turrets, it is a good idea to have a look at the VCTF4 source code which is available on the downloads page.

3 – Adding the vehicle to the map

When you are done with the code and compiled it, you will have a .u file with your vehicle ready to use. Since you probably don't want your map to depend on that file, you should import it.

3.1 – Importing and placing

In the UnrealEd's command line field, enter the following command with your package's name (however, leave myLevel in there, it's the map package!):

OBJ LOAD FILE=MiniScorp.u PACKAGE=myLevel
Due to UnrealEd being stupid at times, you might have to execute this command twice before it works. No idea what the hell it's doing.

You can now create your own vehicle factory or place any of the VCTF4 ones (do not use the stock UT2004 ones) and change the VehicleClass to your vehicle's class. And voilą, we're done!

v-10

Bad-ass! From here on, you might want to adjust the base skin and the color masks (especially their brightness) to fit your desires best.

4 – Releasing your vehicle

You might wonder: "Why all this hassle compiling a new package? Why not code inside UnrealEd's code editor?"

Well, firstly, it sucks. But apart from that, I chose to make this tutorial go the classic compilation way so you have a distributable .u file. You can release that file and others can use your VCTF4 vehicle on their maps or using vehicle replacement mods - and that's a good thing! Community ftw! Of course, people could still rip it from your map, so do whatever you like really. Releasing is just the cooler way if you ask me.

If you think your vehicle port is exceptionally useful (e.g. the Flare Raptor has been missing so far *hint hint*), please feel free to send it to me and I'll mirror it on this site as well!