Introduction
As the title states, this is a tutorial on how to implement melee weapons in the UDK, but it’s actually a little broader than just melee weapons. This is the first part of a three part series where I go over everything necessary to begin development of a third person hack-and-slash style game. Obviously no single tutorial can walk you through writing a game from beginning to end, but I’m hoping this one will get you well on your way. I’m calling it a melee weapon tutorial because that’s where the main focus is, but I’ll be covering several other subjects as well, such as:
- Third person isometric camera
- Archetypes
- General weapon functionality
- AnimSets and AnimTrees
- Particle effects
- Sound Effects
Because I’m a programmer, I’m going to focus on what it takes in the code to make melee weapons happen. I’m not going to spend any time on creating 3D models or animations or particle effects because there are many other tutorials out there that cover those topics and they’re really beyond the scope of what I’m trying to show. I will, however, go over how to use these things in the editor and how to access them through your code. I’ve also provided all the content I’m using in this tutorial (as well as the source code) here, so you don’t have to make it all from scratch just to see the tutorial. I’m also going to assume that you have a basic understanding of Unrealscript and the UDK editor, because if I had to explain every little thing I was talking about, this would be a ten part instead of a three part series.
In part one, I’ll explain how to set up your programming environment using Mavrik Games' very own Project Suite Management Utility and Pixel Mine’s nFringe. I’ll go over setting up the pawn, player controller, weapon, game info and inventory manager classes. I’ll show you how to set up an isometric camera and give the sword to the pawn when the game starts. So, without further ado, let’s get started.
Setup
To set up the project, I recommend using Pixel Mine’s nFringe plugin for Visual Studio and my own home-grown application the Project Suite Management Utility. If you’re never used those tools before and you don’t want to, you can still follow this tutorial as long as you know how to set up your environment. I’m going to go through this tutorial assuming you’re using them though.
The Project Suite Management Utility takes care of all the required setup by creating a Visual Studio solution for your suite and a Visual Studio project for your game, as well as setting up the configuration files and content directories. If this is the first time you’re running the PSMU, it will ask you to select the root directory of your UDK installation. Once you’ve selected the folder, create a project suite. You can call it whatever you want, but mine is called “Tutorial”. Add a game to the suite (mine’s called MeleeWeapon) and select it in the list. Leave the settings at default and click the execute button.
Game Info
I’m going to set up the game to use a third person, free-moving isometric camera like Legend of Zelda: Twilight Princess or Batman: Arkham Asylum. To do this, I need to set up a skeletal mesh for the pawn to use. There are a couple of different ways of doing this, but I like using archetypes because it allows the level designers to change the parameters they need using the editor rather than bugging us programmers to do it in code. I also like them because I can use the editor to adjust the parameters rather than needing to re-compile and re-launch the application to see the changes.
So in the MeleeWeaponGameInfo.uc class, we need a variable to set the pawn’s archetype. We’ll assign that along with the player controller class in the DefaultProperties section.
class MeleeWeaponGameInfo extends UDKGame;
var() const archetype MeleeWeaponPawn PawnArchetype;
DefaultProperties
{
PawnArchetype=MeleeWeaponPawn'MeleeWeaponContent.Archetypes.MeleeWeaponPawn'
PlayerControllerClass=class'MeleeWeaponGame.MeleeWeaponPlayerController'
}
Note: if you're using an older version of the PSMU or if you've just downloaded the code, the last line will need to be PlayerControllerClass=class'MeleeWeapon.MeleeWeaponPlayerController'. The latest version of the PSMU appends the word "Game" to the end of the package names to conform to the naming standard in the Unreal Engine.
Next, we need to add a function to create the pawn based on the archetype when the game spawns the pawn. Add this function to the MeleeWeaponGameInfo.uc class:
function Pawn SpawnDefaultPawnFor(Controller NewPlayer, NavigationPoint StartSpot) { local Pawn SpawnedPawn; if (NewPlayer == none || StartSpot == none) { return none; } SpawnedPawn = Spawn(PawnArchetype.Class,,, StartSpot.Location,, PawnArchetype); return SpawnedPawn; }
The SpawnDefaultPawnFor function gets called when the player controller class is initialized. Basically all that needs to happen is we need to return a pawn spawned according to the PawnArchetype and the game engine takes care of assigning the controller to it.
Pawn
There’s one more function we’ll add to the MeleeWeaponGameInfo class to give the melee weapon to the pawn when the game starts, but before we do we need to set up our pawn and our weapon classes. We’ll start with the pawn, so open up the MeleeWeaponPawn.uc file. We’re going to set up a Skeletal Mesh Component for our pawn to use. We also need to set up how the camera acts as well as a socket name to attach our weapon’s mesh to the pawn’s mesh. Add the following variables to the top of the MeleeWeaponPawn class:
var() const DynamicLightEnvironmentComponent LightEnvironment; var() const int IsoCamAngle; var() const float CamOffsetDistance; var() const Name SwordHandSocketName; var float CamPitch;
The IsoCamAngle will be used to clamp the CamPitch, which is just the pitch component of the camera rotation. CamOffsetDistance is the distance from the camera to the player and the SwordHandSocketName will be assigned the name of the socket located on the pawn’s skeletal mesh where we want the sword to attach (the “WeaponPoint” socket on the human-like skeletal meshes that come with UDK). We set up everything but the CamPitch to be visible to the editor by adding “()” after the “var” declaration. Note that if we write something in between the parentheses for a variable, that variable would show up under a section with that same title in the properties window in the editor. By default though, these variables will just show up under a section with the same name as the class – in this case, “Melee Weapon Pawn”. The CamPitch will be set within code and adjusted every tick, so it doesn’t need to be visible to the editor.
Next, let’s set up the DefaultProperties section. I won’t go into too much detail about what’s going on here except to say that this is where we attach the light environment and skeletal mesh to the pawn. We don’t actually tell it which skeletal mesh to use though – we’ll set that up in the archetype. Think of the DefaultProperties section as the constructor – whenever an Actor gets spawned or an Object gets instantiated, it gets built by assigning its variables according to this section.
DefaultProperties { Components.Remove(Sprite) Begin Object Class=DynamicLightEnvironmentComponent Name=MyLightEnvironment bSynthesizeSHLight=true bIsCharacterLightEnvironment=true
bUseBooleanEnvironmentShadowing=false End Object Components.Add(MyLightEnvironment) LightEnvironment=MyLightEnvironment Begin Object Class=SkeletalMeshComponent Name=MySkeletalMeshComponent bCacheAnimSequenceNodes=false AlwaysLoadOnClient=true AlwaysLoadOnServer=true CastShadow=true BlockRigidBody=true bUpdateSkelWhenNotRendered=false bIgnoreControllersWhenNotRendered=true bUpdateKinematicBonesFromAnimation=true bCastDynamicShadow=true RBChannel=RBCC_Untitled3 RBCollideWithChannels=(Untitled3=true) LightEnvironment=MyLightEnvironment bOverrideAttachmentOwnerVisibility=true bAcceptsDynamicDecals=false bHasPhysicsAssetInstance=true TickGroup=TG_PreAsyncWork MinDistFactorForKinematicUpdate=0.2f bChartDistanceFactor=true RBDominanceGroup=20 Scale=1.f bAllowAmbientOcclusion=false bUseOnePassLightingOnTranslucency=true bPerBoneMotionBlur=true End Object Mesh=MySkeletalMeshComponent Components.Add(MySkeletalMeshComponent) }
Camera
Now let’s work on the camera. An isometric camera is positioned behind and above the player and looks down on him. A fixed isometric camera doesn’t rotate at all, but it follows the players movements. We want ours to be a free-moving camera, meaning it will stay behind the player and rotate with him. In order to get the camera’s position right, you have to use a little bit of math. On a 2D plane it’s not that bad: x = x0 + r * Cos(theta), y = y0 + r * Sin(theta). However, adding the third dimension into the mix makes for a more complicated formula. We also have to deal with negative pitch (the camera will be pointing downward instead of upward), a coordinate system that has the z-axis pointing up and down instead of in and out of the screen, and unreal units instead of radians or degrees. Needless to say, this makes for a royal mess. This isn’t a tutorial on mathematics in games though, so I’ll just give you the code and a very brief explanation of what it does:
simulated function bool CalcCamera(float DeltaTime, out vector out_CamLoc, out rotator out_CamRot, out float out_FOV) { local Vector HitLocation, HitNormal; out_CamLoc = Location; out_CamLoc.X -= Cos(Rotation.Yaw * UnrRotToRad) * Cos(CamPitch * UnrRotToRad) * CamOffsetDistance; out_CamLoc.Y -= Sin(Rotation.Yaw * UnrRotToRad) * Cos(CamPitch * UnrRotToRad) * CamOffsetDistance; out_CamLoc.Z -= Sin(CamPitch * UnrRotToRad) * CamOffsetDistance; out_CamRot.Yaw = Rotation.Yaw; out_CamRot.Pitch = CamPitch; out_CamRot.Roll = 0; if (Trace(HitLocation, HitNormal, out_CamLoc, Location, false, vect(12, 12, 12)) != none) { out_CamLoc = HitLocation; } return true; }
First of all, we move the camera to the player’s location to make that the center of our coordinate system. We use something similar to the formula to calculate a point on a sphere, but we invert it because of all the aforementioned reasons. Also, we use the UnrRotToRad constant to convert our rotation units to radians. The player’s yaw is our theta, the CamPitch our phi, the CamOffsetDistance our radius and viola! The camera is translated to the right point on the sphere around our player. We set the yaw and the pitch on the camera to point it at our player and we set the roll to zero so it always stays upright. The last thing we do is check to make sure that our camera hasn’t bumped into any walls by raytracing from the player’s location to the camera’s. If it hits a wall, we just move it to where the collision took place, padded by a sphere with a 12 unit radius so we don’t see through the wall.
The next step is to adjust the CamPitch when the user moves the mouse up and down. I like to keep all my input handling in the player controller class, so open up MeleeWeaponPlayerController.uc. We just want to tap into the UpdateRotation method to adjust our pawn’s CamPitch variable. We also want to constrain CamPitch so we can’t go around and around the player in vertical circles. We’ll use the IsoCamAngle to clamp it.
function UpdateRotation(float DeltaTime) { local MeleeWeaponPawn MWPawn; super.UpdateRotation(DeltaTime); MWPawn = MeleeWeaponPawn(self.Pawn); if (MWPawn != none) { MWPawn.CamPitch = Clamp(MWPawn.CamPitch + self.PlayerInput.aLookUp, -MWPawn.IsoCamAngle, MWPawn.IsoCamAngle); } }
Weapon
Now we finally get to the weapon. We need to create a class for our melee weapon that extends Weapon. Add a new class to your project called MeleeWeaponSword.uc. For now, let’s just get the sword in the player’s hands and we’ll worry about swinging it later. When the player gets an item, that item’s ClientGivenTo function gets called. When this happens with our sword, we want to attach the sword’s mesh to the weapon socket on our player’s hand. Add this function to the MeleeWeaponSword.uc:
reliable client function ClientGivenTo(Pawn NewOwner, bool bDoNotActivate) { local MeleeWeaponPawn MWPawn; super.ClientGivenTo(NewOwner, bDoNotActivate); MWPawn = MeleeWeaponPawn(NewOwner); if (MWPawn != none && MWPawn.Mesh.GetSocketByName(MWPawn.SwordHandSocketName) != none) { Mesh.SetShadowParent(MWPawn.Mesh); Mesh.SetLightEnvironment(MWPawn.LightEnvironment); MWPawn.Mesh.AttachComponentToSocket(Mesh, MWPawn.SwordHandSocketName); } }
Now we need a skeletal mesh just like we did with the player. Again, we’re going to use an archetype for the weapon so that the properties can easily be tweaked in the editor. Add this to the DefaultProperties of the MeleeWeaponSword:
DefaultProperties { Begin Object Class=SkeletalMeshComponent Name=SwordSkeletalMeshComponent bCacheAnimSequenceNodes=false AlwaysLoadOnClient=true AlwaysLoadOnServer=true CastShadow=true BlockRigidBody=true bUpdateSkelWhenNotRendered=false bIgnoreControllersWhenNotRendered=true bUpdateKinematicBonesFromAnimation=true bCastDynamicShadow=true RBChannel=RBCC_Untitled3 RBCollideWithChannels=(Untitled3=true) bOverrideAttachmentOwnerVisibility=true bAcceptsDynamicDecals=false bHasPhysicsAssetInstance=true TickGroup=TG_PreAsyncWork MinDistFactorForKinematicUpdate=0.2f bChartDistanceFactor=true RBDominanceGroup=20 Scale=1.f bAllowAmbientOcclusion=false bUseOnePassLightingOnTranslucency=true bPerBoneMotionBlur=true End Object Mesh=SwordSkeletalMeshComponent }
I’ll mention again that we’re only attaching the skeletal mesh component to the sword, we’ll pick the actual mesh in the editor and assign it to our archetype.
Inventory Manager
Now we need to be able to give the sword to our player. To do this, we need to write our own Inventory Manager class that extends InventoryManager so that we can spawn the weapon based off of our archetype. Really the only difference between this function and the CreateInventory function in the InventoryManager class is the last parameter to the Spawn function, which is the template from which to build the weapon. We also set the first item in the PendingFire array to “0″ in the DefaultProperties, meaning that this weapon is not waiting to fire. We’ll go into more detail when we get ready to start swinging the sword.
function Inventory CreateInventoryArchetype(Inventory NewInventoryItemArchetype, optional bool bDoNotActivate) { local Inventory Inv; if (NewInventoryItemArchetype != none) { Inv = Spawn(NewInventoryItemArchetype.Class, Owner,,,, NewInventoryItemArchetype); if (Inv != none) {
if (!AddInventory(Inv, bDoNotActivate)) {
Inv.Destroy(); Inv = none; } } } return Inv; } DefaultProperties { PendingFire(0)=0 }
Now we need to actually give the sword to the player when the game starts. Go back into the MeleeWeaponGameInfo class and add a weapon archetype variable beneath the PawnArchetype. Also, you need to initialize it in the DefaultProperties:
var() const archetype MeleeWeaponSword SwordArchetype; ... DefaultProperties { SwordArchetype=MeleeWeaponSword'MeleeWeaponContent.Archetypes.MeleeWeaponSword' ... }
Now we can add the function to give the sword to the player when he spawns:
event AddDefaultInventory(Pawn P) { local MeleeWeaponInventoryManager MWInventoryManager; super.AddDefaultInventory(P); if (SwordArchetype != None) { MWInventoryManager = MeleeWeaponInventoryManager(P.InvManager); if (MWInventoryManager != None) { MWInventoryManager.CreateInventoryArchetype(SwordArchetype, false); } } }
Now set the InventoryManagerClass in the DefaultProperties section of the MeleeWeaponPawn to our new MeleeWeaponInventoryManager:
DefaultProperties { InventoryManagerClass=class'MeleeWeaponInventoryManager' ... }
That’s it for part one. In part two, I’ll talk about how weapons work in UDK and how we’re going to manipulate that system for our melee weapon. I’ll go into more detail about how archetypes work and show you how to use them in the editor. We’ll add meshes to our sword and pawn, and build a simple world with some objects in it to test out our sword. Finally, I’ll show you how to actually swing the sword and apply momentum and damage to whatever it hits.