Edit 10/1/2020: Pictures are small/low res if you don't login
I noticed there are a lot of general guides on how to get started in the KF2 modding sphere, but a severe lack of anything that goes completely in depth
This has frustrated me to no end, as any problems I run by have to be solved by hand, wasting hours of my time generally on simple solutions
So, I wanted to just post this here to vent my frustration and maybe help someone else. I will be giving a complete guide on how I created a new elite husk that freezes the player, including the code and how to make a few of FX from scratch.
But before that, I want to address something that really threw me off when I first started. The amount of files there are. There's a lot to dig into, but luckily it can be pretty easy to discern what they do based on the prefix of the filename.
For example: KFPawn are pawn entities (players or ai), and usually handle interactions with other pawn entities and the environment. KFProj are projectile entities which handle things like the what type of model to use for a projectile, the damage type and explosion archetype used for that projectile. KFDT are damage types, which handle things like affliction (freeze or burn amount) and actual damage ranges. You get the idea. I'm not going to list every prefix, but the lesson is that you can generally tell what a class does by it's prefix, which leads to an easier time following what the inner parts of a class actually does.
I would also recommend getting Agent Ransack. It is an extremely effective way to search within files for specific character sequences (ie. looking for a variable name in another file). you can simply setup agent ransack in the SRC file and search all files for a function call or variable name within a couple seconds.
Assuming we have setup all our modding tools and have a basic setup for some mutator we begin with creating a new human pawn. Every pawn has a special moves, some moves are inherited from their parent classes (many of the zeds inherit things like stunned or frozen special moves from KFPawn_Monster), wheras other types are given to the specific class (Like how a husk has a StandAndFire special move). These moves generally are called when a specific requirement is met. To begin, we will need to setup a new KFPawn_Human within our mutator.
I did this within PostBeginPlay(). PostBeginPlay is a function that handles the game actors after they have been initialized. This will initialize the human actor as your new class that you will create.
Now we want to create a new file called KFPawn_MyNewHuman.uc. UC filetypes must have the same name as the class name, otherwise the compiler throws a fit, likely because it uses the name as a pseudo-pointer. Within our new human file we put
Look at that, comments. While I'm on the topic, please for gods sake put comments in your code. I code for a living and man it helps other people look at it. Even with the UC scripting files where variable names are pretty much required to be very specific (otherwise you may reference a parent function or variable by accident), it's hard to discern why coders did some of the things they do, whether it be workarounds, or the ever dreaded "I don't get why this works, but it does, please don't remove it".
Back to our actual program. You may see IncapSettings and wonder "Why is that there?". IncapSettings is a function that handles vulnarability and cooldown to an affliction (mind you, not duration, but that variable can be called on). It will make more sense later, but I put this here now as I don't want to go back and forth between files. What really matters is the new object. Now I'm going to assume you know what an object is, but for those of you who don't know, the TLDR breakdown is that it is a inventory of features that make up some 'thing'. In our case, we are appending a new feature to the human, the fact that he can be frozen.
Now, if you are observant, you may have noticed the 'KFSM' class. That's the prefixes that I was talking about earlier. This one stands for 'Killing floor special moves', which usually means the class has special moves as it's parent. This one is a custom one that I will be going over in a bit, after I address SM_Frozen.
SM_Frozen is a part of a enum list, where enum stands for "Enumeration". Enumerations are basically a way of asscociating a value (0-n) with a another type. This is strictly for the coder, as it allows you to input something that is easier to recognize than some integer value for something like a list/array index. Where does this enum come from? I have honestly forgotten while writing this, but this is why we have Agent Ransack(yay!). The original definition comes from KFPawn, but most of the actual work comes from the affliction manager. When the pawns freeze limit is exceeded and that pawn can also "Do SM_Frozen" then it allows for the special move to be processed. Hey, that's the same place where IncapSettings is messed with! yessir, that's exactly why we call incapsettings, vulnerability acts as a multiplier for this 'FreezePower', but if we don't handle incapsettings at all, then the affliction manager will assume that the pawn is immune to this affliction type.
Now, moving on to my super secret special move, KFSM_HumanFreeze. This is probably one of the larger files, so i'll kinda skim through it and hopefully the comments will clear some things up.
Where to even begin? well, lets start with defaultproperties. This is the stuff that's called when the class is initialized (I assume). A lot of the disable booleans should make sense (except lock pawn and disablesteering, honestly it feels like these do the same thing, which is nothing). These are variables that are created in the parent class and are used to determine what effects to apply to the player when the special move is called. Below that are the FX. AkEvents are sound banks that are played when these events are called. What are sound banks you may ask? They are a library of different sounds that can get called upon (This is why snow sounds different every time you step in the game). In this case we have something like 'WW_UI_PlayerCharacter' which stands for 'WWise' (The audio program) 'User Interface Player Character'. The dot DOES NOT INDICATE YOU ARE CALLING INTO THE OBJECT 'WW_UI_PlayerCharacter'. I program in C a lot and man, that was my first trip up that NO ONE addressed. The dot simply indicates that you are either entering a package (a packet of different stuffs, like AKBanks), or entering a new file (you can call into a specific file like HALOWEEN_). So this statements means that I am grabbing Play_Grab_Stop in the package WW_UI_PlayerCharacter.
KFCameraLensEmit is another class which simply consists of the effect of a lens effect that shows the player is frozen. (I'll go over that later I guess)
Okay, up to the top of the file, SpecialMoveStarted. This function is called, uh, pretty much whenever. You could, if you want, setup your own call for special move started, I instead hijacked KFAFflictionBase's Accrue function that already has a parameter setup for calling a special move when a threshold is met. This can be done by calling AccrueAffliction whenever your ZED or Weapon deals damage, and accruing your affliction type.
Theres something in this function I would like to address, which is that the super call. It's nothing complex, it simply calls the parent function of the same name, so that it can setup variables before everything else is done in the child. There is also the two local variable creations, these are essentially creating a variable that is the type of a local class that can be used as an object to call from, like creating a localc KFPawn_Human as KFPH, then setting that variable to the current controller. Now, I can just type KFPH instead of KFPawn_Human(PawnOwner). PawnOwner in this case is the owner of the special move, ie, the human pawn.
Hopefully the comments explain the rest of what StartSpecialMove is doing, so I'll move onto DoFreeze. SetTimer should hopefully explain itself, but if it doesn't, it's a function that ticks a timer that's in seconds down. When it goes to 0, the function specified is called, which in this case is DoThaw().
After setting the timer, we call the magic if statement, if ( x != NM_DedicatedServer ). All this statement does is check the side of the operation. If we wan't to save resources, why would we play an animation server side? The server doesn't care, it's not human (not yet atleast). So we check if it's not the dedicated server, and if not, we play the FX there.
Now, this is where KF2 SDK starts to really trickle in. Particle systems are a set animation that plays in either a burst, or a loop (or a loop of bursts). Fire is a particle system, which loops based on the current time since the program has started.
OKAY. Before I get yelled at for this, I realize that the prefix for the name of my package is 'KFP', which has no reasonable spot in a FX setting. Why is it called that then? well, welcome to KF2 SDK, where jankiness is king. For some god awful reason, I can't rename, move, delete or export packages, unless I wait long enough. Then I can rename one package, and ONLY one package, before it won't let me again. So I called it KFP at the start, and by golly, I'm too damn lazy to rename it or move files in and out of it.
Ignoring all that, what does these four lines of code do? Well, first we create a new class for a particle system and define it, which will limit everything to one particle system. then, we set that systems template to FX_Human_Freeze, which is a neat effect i added which is a 3rd person model to indicate when a human is frozen. I will go on a tangent on why I can't just freeze the human like the zeds freeze, but in a minute. Now, on the KFPOwner (our specific human) we attach to the mesh(the player model)'s root bone (a unmoving position on the character model) our new system, and then activate it.
TANGENT. Why you can't just freeze the player like you do the zed. A. no groundwork setup for freezing players. B I'll let my comments do the talking
//CharacterMICs[0].SetScalarParameterValue('Scalar_Ice', 1.f);
// What are scalar parameters, and why won't this work?
// scalar parameters are attached changes to the textures, based on the
// math operations done in a material, this is why they are called
// MIC's as they are Material Instance Constants, which allow constant changes to a materials parameters
// unfortunately, the base material that controls things like the
// EMP effect or burned effect (CHR_Basic_PM) cannot be changed in the code
// as it is not an MIC, but a material
// the only way around this would be to create a copy of the CHR_Basic_PM
// then re-reference a copy of all MIC's that reference the base material
// then copy all existing ARCH's that use these MIC's (Which is all the cosmetics, 400+ per character)
// this is possible but takes way too long and would force the player to load in basically reinstall
// all assets again just for freeze effect (please based tripwire, allow more customization)
Yea. I was pretty annoyed at the time.
Tangent mode off. We now go to the DoThaw() function. Literally all this does is call SpecialMoveEnded. I was kinda scared of putting a function like that within a timer, so I just attached it to another function. You can use logic described previously and my comments to infer what everything is doing within the function. What's important to note is that the super calls the parent SpecialmoveEnded function, which sets everything up to stop all the effects on the player.
So there we have it, a whole special move that freezes the player, everything from here on out is pretty much just busy work. I created my own damage type called ZedFreeze
But all it really doees is add a camera effect for when the player is hit, so you can really just use KFDT freeze.
but if you really want to know what I did to make the effect appear
It's just setting a particle system right in front of your face, with a lifespan that matches the special move length.
Okay. So this is all great, we got a human setup that can be frozen, and we have a damage type that can actually freeze. What about the zed? Let's jump into that.
lets go back to our initial mutator
Before we get onto this, for full clarity, I basically stole a method of handling spawning from Forrest Mark X's Infernal Nightmare Mutator. Big Shout out to him as his source code made a lot of things easier to understand (but dude, comment your code, c'mon). I don't really want to go into that right now, as don't really fully understand it myself. I know, I know, kind of a cop-out when it comes to a guide, I may come back to it later but I'd rather describe What I know.
A MUCH easier method to doing this is not doing a mutator, but instead a game mode
Checkout : https://forums.tripwireinteractive....-how-to-mod-kf2-with-a-gameinfo-class.118960/
As a new game mode extends KFGameInfo_Survival.uc, all you need to do is replace indexes in the AIClassList under defaultproperties, as he describes in his tutorial
now, we simply create our own husk class:.
Now, I want you to ignore the spooky vector paramter value, it will make sense later what this is. However, it is important to note what UpdateGameplayMICParams() is. This is called through PostBeginPlay, (as described earleir, AFTER everything is initialized), the function itself will update the Material Instance Constant during the game. Material Instance Constant are a series of parameters that change how a material looks in game. Just keep this in mind for later.
Regardless, we added a new elite class. I emptied the list, as attached to the base husk are already the Edars. You don't really need to do this.
We can now create our new elite husk (Finally!)
Now, your first impression of this code may be "Wow, you hijacked a lot of stuff". Yes, yes I did. The SDK is sometimes really frustrating to use, and it's also a massive time sink to make FX. So I just hijacked some (Mind you, I created a lot of my own)
So, in this, I would like to go from the top, down, as I would like to address what ANIMNOTIFY is. Now, I'm not going to be as thorugh in the rest of these classes, as I'm going to assume stuff is staring to make sense. In the game, there are animation notifications that take place in animation libraries, so during a specific point in an animation, you can notify the back end when some event has finished. That's all this is.
I would also like to address what an archetype, or ARCH is. An archetype is a culmination of different properties that makes up a character (which are usually attached to pawns). They have the mesh, FX's added to the mesh and animation trees.
Finally, I will talk about impactinfo. This class called contains a bunch of information of velocty distributed to the pawn, damage type and also extra functions added on top that are called when a pawn is impacted.
Moving on, we will finally talk about the damage types and moves that are called within this class:
KFSM_Husk_FreezeThrowerAttack
KFProj_Husk_Freezeball
KFDT_Husk_FreezeSuicide
"Wow, vertical, you basically copied KFSM_Husk_FlameThrowerAttack.uc". Yup. I didn't really need to. Let this be a lesson for effeciency, as I could have easily extended KFSM_Husk_FlameThrowerAttack and simply simulated the TurnOnFlamethrower function. I did this instead because this was the last thing I needed to do, and I knew this worked. The issue with the base TurnOnFlamethrower is that:
MyFlameSpray.ImpactProjectileClass = class'KFProj_HuskGroundFire';
This class is not set in the defaultproperties, which means if I wanted to change the damage type, gatta change that function up. I thought to myself "well, whatever" and just copy pastad the whole code over. Definitely going to refactor this when I'm not exhausted of SDK.
Thankfully, I had the consciounce to do this better.
This is really the more important of the three, as it finally brings everything full circle. PlayImpactHitEffects is called when the projectile impacts an object. A lot of the above stuff is fluff, which shatters enemies or applies some scalar_ice paramater to the MIC. What's important is the AccrueAffliction. AF_Freeze is the affliction type asscociated with the freeze affliction, what we are doing is accruing 60 freeze_power to the AF_Freeze affliction type. because our human is vulnerable to 4X freeze, this will instantly overreach the threshold and wabam, our human is now locked in place for 2 seconds.
I'm going to end the guide here. I know I said I would talk about SDK effects, I will still do that, but probably as an addendum at anotehr time. This was a lot longer than I expected. post questions! Point out my Mistakes! HUZZAH!
I noticed there are a lot of general guides on how to get started in the KF2 modding sphere, but a severe lack of anything that goes completely in depth
This has frustrated me to no end, as any problems I run by have to be solved by hand, wasting hours of my time generally on simple solutions
So, I wanted to just post this here to vent my frustration and maybe help someone else. I will be giving a complete guide on how I created a new elite husk that freezes the player, including the code and how to make a few of FX from scratch.
But before that, I want to address something that really threw me off when I first started. The amount of files there are. There's a lot to dig into, but luckily it can be pretty easy to discern what they do based on the prefix of the filename.
For example: KFPawn are pawn entities (players or ai), and usually handle interactions with other pawn entities and the environment. KFProj are projectile entities which handle things like the what type of model to use for a projectile, the damage type and explosion archetype used for that projectile. KFDT are damage types, which handle things like affliction (freeze or burn amount) and actual damage ranges. You get the idea. I'm not going to list every prefix, but the lesson is that you can generally tell what a class does by it's prefix, which leads to an easier time following what the inner parts of a class actually does.
I would also recommend getting Agent Ransack. It is an extremely effective way to search within files for specific character sequences (ie. looking for a variable name in another file). you can simply setup agent ransack in the SRC file and search all files for a function call or variable name within a couple seconds.
Assuming we have setup all our modding tools and have a basic setup for some mutator we begin with creating a new human pawn. Every pawn has a special moves, some moves are inherited from their parent classes (many of the zeds inherit things like stunned or frozen special moves from KFPawn_Monster), wheras other types are given to the specific class (Like how a husk has a StandAndFire special move). These moves generally are called when a specific requirement is met. To begin, we will need to setup a new KFPawn_Human within our mutator.
Code:
local KFGameInfo KFGI;
KFGI = KFGameInfo(WorldInfo.Game);
KFGI.DefaultPawnClass=class'ihd_edit.KFPawn_MyNewHuman';
I did this within PostBeginPlay(). PostBeginPlay is a function that handles the game actors after they have been initialized. This will initialize the human actor as your new class that you will create.
Now we want to create a new file called KFPawn_MyNewHuman.uc. UC filetypes must have the same name as the class name, otherwise the compiler throws a fit, likely because it uses the name as a pseudo-pointer. Within our new human file we put
Code:
class KFPawn_MyNewHuman extends KFPawn_Human;
defaultproperties
{
// set the incap settings otherwise the affliction manager will think the player is immune
IncapSettings(AF_Freeze)=(Vulnerability=(4.f), Cooldown=10.0, Duration=2.0)
//`log("default properties run")
// add a new special move for being frozen
Begin Object Name=SpecialMoveHandler_0
SpecialMoveClasses(SM_Frozen)=class'ihd_edit.KFSM_HumanFreeze'
End Object
}
Look at that, comments. While I'm on the topic, please for gods sake put comments in your code. I code for a living and man it helps other people look at it. Even with the UC scripting files where variable names are pretty much required to be very specific (otherwise you may reference a parent function or variable by accident), it's hard to discern why coders did some of the things they do, whether it be workarounds, or the ever dreaded "I don't get why this works, but it does, please don't remove it".
Back to our actual program. You may see IncapSettings and wonder "Why is that there?". IncapSettings is a function that handles vulnarability and cooldown to an affliction (mind you, not duration, but that variable can be called on). It will make more sense later, but I put this here now as I don't want to go back and forth between files. What really matters is the new object. Now I'm going to assume you know what an object is, but for those of you who don't know, the TLDR breakdown is that it is a inventory of features that make up some 'thing'. In our case, we are appending a new feature to the human, the fact that he can be frozen.
Now, if you are observant, you may have noticed the 'KFSM' class. That's the prefixes that I was talking about earlier. This one stands for 'Killing floor special moves', which usually means the class has special moves as it's parent. This one is a custom one that I will be going over in a bit, after I address SM_Frozen.
SM_Frozen is a part of a enum list, where enum stands for "Enumeration". Enumerations are basically a way of asscociating a value (0-n) with a another type. This is strictly for the coder, as it allows you to input something that is easier to recognize than some integer value for something like a list/array index. Where does this enum come from? I have honestly forgotten while writing this, but this is why we have Agent Ransack(yay!). The original definition comes from KFPawn, but most of the actual work comes from the affliction manager. When the pawns freeze limit is exceeded and that pawn can also "Do SM_Frozen" then it allows for the special move to be processed. Hey, that's the same place where IncapSettings is messed with! yessir, that's exactly why we call incapsettings, vulnerability acts as a multiplier for this 'FreezePower', but if we don't handle incapsettings at all, then the affliction manager will assume that the pawn is immune to this affliction type.
Now, moving on to my super secret special move, KFSM_HumanFreeze. This is probably one of the larger files, so i'll kinda skim through it and hopefully the comments will clear some things up.
Code:
class KFSM_HumanFreeze extends KFSpecialMove;
var transient KFPlayerController OwnerController;
var class<EmitterCameraLensEffectBase> LensEffectTemplate; // a template effect for the user camera, ie; FullFrost
var protected ParticleSystem FrozenSteamTemplate; // particle system for frozen block
var ParticleSystemComponent FrozenHuman;
var AkEvent GrabbedSoundModeStartEvent; // victim grapple special move sound begin
var AkEvent GrabbedSoundModeEndEvent;
var AkEvent ShatterSound; // freeze grenade explosion sound
/** Notification called when Special Move starts **/
function SpecialMoveStarted(bool bForced, Name PrevMove )
{
local KFPawn_Human KFPH;
local KFGameInfo KFGI;
super.SpecialMoveStarted( bForced, PrevMove );
OwnerController = KFPlayerController( PawnOwner.Controller );
KFPH = KFPawn_Human(PawnOwner);
// if the owner is indeed a human
if (KFPH != none)
{
if (PawnOwner.Role == ROLE_Authority)
{
KFGI = KFGameInfo(KFPH.WorldInfo.Game);
if (KFGI != none && KFGI.DialogManager != none)
{
// play the grabbed sound dialogue
KFGI.DialogManager.PlayPlayerGrabbedDialog(KFPH);
}
}
}
// if there is a controller
if( OwnerController != none )
{
// check if it's client
if( OwnerController.IsLocalController() )
{
// play grabbed sound and apply frost lense
OwnerController.PostAkEvent(GrabbedSoundModeStartEvent,,,true);
OwnerController.ClientSpawnCameraLensEffect(LensEffectTemplate);
}
}
// check if the owner is crouched
KFPOwner.ShouldCrouch( false );
if( KFPOwner.bIsCrouched )
{
// if they are, uncrouch them
KFPOwner.ForceUnCrouch();
}
// play the shatter sound to indicate that the freezing has started
KFPOwner.PlaySoundBase(default.ShatterSound, true,,, KFPOwner.Location);
// disable the players ability to crouch and jump
KFPOwner.bCanCrouch = false;
KFPOwner.bCanJump = false;
KFPOwner.bJumpCapable = false;
DoFreeze();
}
/** create timers to determine how long the player is frozen for **/
function DoFreeze()
{
local float TimeUntilThaw;
if ( KFPOwner.Role == ROLE_Authority )
{
// wait 2 seconds untill calling function that stops the special move
TimeUntilThaw = 2.f;
KFPOwner.SetTimer(TimeUntilThaw, false, nameof(DoThaw), self);
}
// client side
if ( PawnOwner.WorldInfo.NetMode != NM_DedicatedServer )
{
// create a particle system, which is a frozen block
FrozenHuman = new(self) class'ParticleSystemComponent';
FrozenHuman.SetTemplate( ParticleSystem'KFP_CryoTrail.FX_Human_Freeze' );
KFPOwner.Mesh.AttachComponentToSocket( FrozenHuman, 'root' );
FrozenHuman.ActivateSystem();
}
}
function DoThaw()
{
if ( PawnOwner.Role == ROLE_Authority )
{
// end the special move
KFPOwner.EndSpecialMove();
}
}
/** called when the DoThaw timer ends **/
function SpecialMoveEnded(Name PrevMove, Name NextMove)
{
super.SpecialMoveEnded( PrevMove, NextMove );
`log("I have thawed");
if ( KFPOwner.WorldInfo.NetMode != NM_DedicatedServer )
{
// remove the particle system of the frozen block
if( FrozenHuman!=None )
FrozenHuman.SetStopSpawning( -1, true );
}
if( OwnerController != none )
{
if( OwnerController.IsLocalController() )
{
// end the player grabbed sound and the frost camera template
OwnerController.PostAkEvent(GrabbedSoundModeEndEvent);
OwnerController.ClientRemoveCameraLensEffect(LensEffectTemplate);
}
}
// clear the timer for the thaw event
KFPOwner.ClearTimer( nameof(DoThaw), self );
// allow player full control
KFPOwner.bCanCrouch = true;
KFPOwner.bCanJump = true;
KFPOwner.bJumpCapable = true;
}
defaultproperties
{
Handle=KFSM_Frozen
// when the parent DoSpecialMove super is called, these parameters lock the player in place
bDisableMovement=true
bDisableSteering=true
bDisableWeaponInteraction=true
bLockPawnRotation=true
bDisableLook=true
// templates for various FX
GrabbedSoundModeStartEvent=AkEvent'WW_UI_PlayerCharacter.Play_Grab_Start'
GrabbedSoundModeEndEvent=AkEvent'WW_UI_PlayerCharacter.Play_Grab_Stop'
ShatterSound=AkEvent'WW_WEP_Freeze_Grenade.Play_Freeze_Grenade_Shatter'
LensEffectTemplate=class'KFCameraLensEmit_FullFrost'
}
Where to even begin? well, lets start with defaultproperties. This is the stuff that's called when the class is initialized (I assume). A lot of the disable booleans should make sense (except lock pawn and disablesteering, honestly it feels like these do the same thing, which is nothing). These are variables that are created in the parent class and are used to determine what effects to apply to the player when the special move is called. Below that are the FX. AkEvents are sound banks that are played when these events are called. What are sound banks you may ask? They are a library of different sounds that can get called upon (This is why snow sounds different every time you step in the game). In this case we have something like 'WW_UI_PlayerCharacter' which stands for 'WWise' (The audio program) 'User Interface Player Character'. The dot DOES NOT INDICATE YOU ARE CALLING INTO THE OBJECT 'WW_UI_PlayerCharacter'. I program in C a lot and man, that was my first trip up that NO ONE addressed. The dot simply indicates that you are either entering a package (a packet of different stuffs, like AKBanks), or entering a new file (you can call into a specific file like HALOWEEN_). So this statements means that I am grabbing Play_Grab_Stop in the package WW_UI_PlayerCharacter.
KFCameraLensEmit is another class which simply consists of the effect of a lens effect that shows the player is frozen. (I'll go over that later I guess)
Okay, up to the top of the file, SpecialMoveStarted. This function is called, uh, pretty much whenever. You could, if you want, setup your own call for special move started, I instead hijacked KFAFflictionBase's Accrue function that already has a parameter setup for calling a special move when a threshold is met. This can be done by calling AccrueAffliction whenever your ZED or Weapon deals damage, and accruing your affliction type.
Theres something in this function I would like to address, which is that the super call. It's nothing complex, it simply calls the parent function of the same name, so that it can setup variables before everything else is done in the child. There is also the two local variable creations, these are essentially creating a variable that is the type of a local class that can be used as an object to call from, like creating a localc KFPawn_Human as KFPH, then setting that variable to the current controller. Now, I can just type KFPH instead of KFPawn_Human(PawnOwner). PawnOwner in this case is the owner of the special move, ie, the human pawn.
Hopefully the comments explain the rest of what StartSpecialMove is doing, so I'll move onto DoFreeze. SetTimer should hopefully explain itself, but if it doesn't, it's a function that ticks a timer that's in seconds down. When it goes to 0, the function specified is called, which in this case is DoThaw().
After setting the timer, we call the magic if statement, if ( x != NM_DedicatedServer ). All this statement does is check the side of the operation. If we wan't to save resources, why would we play an animation server side? The server doesn't care, it's not human (not yet atleast). So we check if it's not the dedicated server, and if not, we play the FX there.
Now, this is where KF2 SDK starts to really trickle in. Particle systems are a set animation that plays in either a burst, or a loop (or a loop of bursts). Fire is a particle system, which loops based on the current time since the program has started.
OKAY. Before I get yelled at for this, I realize that the prefix for the name of my package is 'KFP', which has no reasonable spot in a FX setting. Why is it called that then? well, welcome to KF2 SDK, where jankiness is king. For some god awful reason, I can't rename, move, delete or export packages, unless I wait long enough. Then I can rename one package, and ONLY one package, before it won't let me again. So I called it KFP at the start, and by golly, I'm too damn lazy to rename it or move files in and out of it.
Ignoring all that, what does these four lines of code do? Well, first we create a new class for a particle system and define it, which will limit everything to one particle system. then, we set that systems template to FX_Human_Freeze, which is a neat effect i added which is a 3rd person model to indicate when a human is frozen. I will go on a tangent on why I can't just freeze the human like the zeds freeze, but in a minute. Now, on the KFPOwner (our specific human) we attach to the mesh(the player model)'s root bone (a unmoving position on the character model) our new system, and then activate it.
TANGENT. Why you can't just freeze the player like you do the zed. A. no groundwork setup for freezing players. B I'll let my comments do the talking
//CharacterMICs[0].SetScalarParameterValue('Scalar_Ice', 1.f);
// What are scalar parameters, and why won't this work?
// scalar parameters are attached changes to the textures, based on the
// math operations done in a material, this is why they are called
// MIC's as they are Material Instance Constants, which allow constant changes to a materials parameters
// unfortunately, the base material that controls things like the
// EMP effect or burned effect (CHR_Basic_PM) cannot be changed in the code
// as it is not an MIC, but a material
// the only way around this would be to create a copy of the CHR_Basic_PM
// then re-reference a copy of all MIC's that reference the base material
// then copy all existing ARCH's that use these MIC's (Which is all the cosmetics, 400+ per character)
// this is possible but takes way too long and would force the player to load in basically reinstall
// all assets again just for freeze effect (please based tripwire, allow more customization)
Yea. I was pretty annoyed at the time.
Tangent mode off. We now go to the DoThaw() function. Literally all this does is call SpecialMoveEnded. I was kinda scared of putting a function like that within a timer, so I just attached it to another function. You can use logic described previously and my comments to infer what everything is doing within the function. What's important to note is that the super calls the parent SpecialmoveEnded function, which sets everything up to stop all the effects on the player.
So there we have it, a whole special move that freezes the player, everything from here on out is pretty much just busy work. I created my own damage type called ZedFreeze
Code:
class KFDT_ZedFreeze extends KFDT_Freeze;
// this class will determine what emmiter effects play what a damage type is dealt
defaultproperties
{
CameraLensEffectTemplate=class'KFCameraLensEmit_Frost'
}
But all it really doees is add a camera effect for when the player is hit, so you can really just use KFDT freeze.
but if you really want to know what I did to make the effect appear
Code:
class KFCameraLensEmit_FullFrost extends KFEmit_CameraEffect;
defaultproperties
{
PS_CameraEffect=ParticleSystem'KFP_CryoTrail.FX_Camera_FullFreeze'
// disallos multiple instances of the emmiter, as we will not call this again
bAllowMultipleInstances=false
LifeSpan=2.0f
bDepthTestEnabled=false
}
It's just setting a particle system right in front of your face, with a lifespan that matches the special move length.
Okay. So this is all great, we got a human setup that can be frozen, and we have a damage type that can actually freeze. What about the zed? Let's jump into that.
lets go back to our initial mutator
Before we get onto this, for full clarity, I basically stole a method of handling spawning from Forrest Mark X's Infernal Nightmare Mutator. Big Shout out to him as his source code made a lot of things easier to understand (but dude, comment your code, c'mon). I don't really want to go into that right now, as don't really fully understand it myself. I know, I know, kind of a cop-out when it comes to a guide, I may come back to it later but I'd rather describe What I know.
A MUCH easier method to doing this is not doing a mutator, but instead a game mode
Checkout : https://forums.tripwireinteractive....-how-to-mod-kf2-with-a-gameinfo-class.118960/
As a new game mode extends KFGameInfo_Survival.uc, all you need to do is replace indexes in the AIClassList under defaultproperties, as he describes in his tutorial
Code:
defaultproperties
{
AIClassList(AT_Husk)=class'KFGameContent.KFPawn_ZedHusk_Myown'
}
now, we simply create our own husk class:.
Code:
class KFPawn_ZedHusk_Myown extends KFPawn_ZedHusk;
var LinearColor AVector;
simulated function PostBeginPlay()
{
Super.PostBeginPlay();
if( WorldInfo.NetMode!=NM_DedicatedServer )
{
UpdateGameplayMICParams();
}
}
simulated function UpdateGameplayMICParams()
{
Super.UpdateGameplayMICParams();
if( WorldInfo.NetMode!=NM_DedicatedServer )
{
CharacterMICs[0].SetVectorParameterValue('Vector_GlowColor', AVector);
}
}
DefaultProperties
{
AVector=(B=1,G=0.2f)
ElitePawnClass.Empty
ElitePawnClass.Add(class'KFPawn_ZedFrozenHusk')
}
Now, I want you to ignore the spooky vector paramter value, it will make sense later what this is. However, it is important to note what UpdateGameplayMICParams() is. This is called through PostBeginPlay, (as described earleir, AFTER everything is initialized), the function itself will update the Material Instance Constant during the game. Material Instance Constant are a series of parameters that change how a material looks in game. Just keep this in mind for later.
Regardless, we added a new elite class. I emptied the list, as attached to the base husk are already the Edars. You don't really need to do this.
We can now create our new elite husk (Finally!)
Code:
class KFPawn_ZedFrozenHusk extends KFPawn_ZedHusk;
var LinearColor MainGlowColor;
simulated function PostBeginPlay()
{
Super.PostBeginPlay();
if( WorldInfo.NetMode!=NM_DedicatedServer )
{
UpdateGameplayMICParams();
}
}
simulated function UpdateGameplayMICParams()
{
Super.UpdateGameplayMICParams();
if( WorldInfo.NetMode!=NM_DedicatedServer )
{
CharacterMICs[0].SetVectorParameterValue('Vector_GlowColor', MainGlowColor);
CharacterMICs[0].SetVectorParameterValue('Vector_FresnelGlowColor', MainGlowColor);
CharacterMICs[0].SetScalarParameterValue('Scalar_Ice', 0.25f);
}
}
/** Turns medium range flamethrower effect on */
simulated function ANIMNOTIFY_FlameThrowerOn()
{
if( IsDoingSpecialMove(SM_HoseWeaponAttack) )
{
KFSM_Husk_FreezeThrowerAttack(SpecialMoves[SpecialMove]).TurnOnFlamethrower();
}
}
/** Turns medium range flamethrower effect off */
simulated function ANIMNOTIFY_FlameThrowerOff()
{
if( IsDoingSpecialMove(SM_HoseWeaponAttack) )
{
KFSM_Husk_FreezeThrowerAttack(SpecialMoves[SpecialMove]).TurnOffFlamethrower();
}
}
DefaultProperties
{
MainGlowColor=(B=1,G=0.66f,R=0.33)
FireballClass= class'ihd_edit.KFProj_Husk_Freezeball'
// dinnae what does it, but some special seasonal handler adds HALLOWEEN_ to the beginning
// based on the SEI_ID
// A. very poor coding practice, why not just hold the seasonal contents in a folder and then
// assign index values to folder names in a native function? pretty common practice in C
// B. I suspect it's the boss cache as that's the only function that messes with MonsterArchPath
// but it's native(C++) so whatever
MonsterArchPath="Frozen_ZED_ARCH.ZED_Husk_Archetype"
Begin Object Name=ChestLightComponent0
LightColor=(R=86,G=167,B=255,A=255)
End Object
// I am the master of copy codes
// speaking of poor coding practice, I basically hijacked this from the freeze grenades
// Grenade explosion light
Begin Object Name=ExplosionPointLight
LightColor=(R=128,G=200,B=255,A=255)
End Object
// explosion
Begin Object Class=KFGameExplosion Name=ExploTemplate0
Damage=60 //100
DamageRadius=500//600
DamageFalloffExponent=0.5f //2
DamageDelay=0.f
// Damage Effects
MyDamageType=class'KFDT_Husk_FreezeSuicide'
FractureMeshRadius=200.0
FracturePartVel=500.0
ExplosionEffects=KFImpactEffectInfo'WEP_Freeze_Grenade_Arch.FreezeGrenade_Explosion'
ExplosionSound=AkEvent'WW_WEP_Freeze_Grenade.Play_Freeze_Grenade_Explo'
MomentumTransferScale=1
// Dynamic Light
ExploLight=ExplosionPointLight
ExploLightStartFadeOutTime=0.5
ExploLightFadeOutTime=0.25
ExploLightFlickerIntensity=5.f
ExploLightFlickerInterpSpeed=15.f
// Camera Shake
CamShake=CameraShake'FX_CameraShake_Arch.Grenades.Default_Grenade'
CamShakeInnerRadius=200
CamShakeOuterRadius=900
CamShakeFalloff=1.5f
bOrientCameraShakeTowardsEpicenter=true
End Object
ExplosionTemplate=ExploTemplate0
Begin Object Name=SpecialMoveHandler_0
SpecialMoveClasses(SM_HoseWeaponAttack)= class'KFSM_Husk_FreezeThrowerAttack'
End Object
ElitePawnClass.Empty
}
Now, your first impression of this code may be "Wow, you hijacked a lot of stuff". Yes, yes I did. The SDK is sometimes really frustrating to use, and it's also a massive time sink to make FX. So I just hijacked some (Mind you, I created a lot of my own)
So, in this, I would like to go from the top, down, as I would like to address what ANIMNOTIFY is. Now, I'm not going to be as thorugh in the rest of these classes, as I'm going to assume stuff is staring to make sense. In the game, there are animation notifications that take place in animation libraries, so during a specific point in an animation, you can notify the back end when some event has finished. That's all this is.
I would also like to address what an archetype, or ARCH is. An archetype is a culmination of different properties that makes up a character (which are usually attached to pawns). They have the mesh, FX's added to the mesh and animation trees.
Finally, I will talk about impactinfo. This class called contains a bunch of information of velocty distributed to the pawn, damage type and also extra functions added on top that are called when a pawn is impacted.
Moving on, we will finally talk about the damage types and moves that are called within this class:
KFSM_Husk_FreezeThrowerAttack
KFProj_Husk_Freezeball
KFDT_Husk_FreezeSuicide
Code:
class KFSM_Husk_FreezeThrowerAttack extends KFSM_PlaySingleAnim;
/** The Archetype to spawn for our fire spray actors. */
var KFSprayActor FlameSprayArchetype;
var KFSprayActor MyFlameSpray;
/** Emitter to play when firing stops. */
var ParticleSystemComponent PSC_EndSpray;
/** Replicated flag to turn off the flamethrower effect on clients. */
var bool bFlameThrowerActive;
/** Pilot light sound play event */
var AkEvent FlameAttackPlayEvent;
/** Pilot light sound stop event */
var AkEvent FlameAttackStopEvent;
protected function bool InternalCanDoSpecialMove()
{
local vector HitLocation, HitNormal;
local Actor HitActor;
if( KFPOwner.IsHumanControlled() )
{
return KFPOwner.IsCombatCapable();
}
if( AIOwner == none || AIOwner.MyKFPawn == none || AIOwner.Enemy == none )
{
return false;
}
if( !KFPOwner.IsCombatCapable() )
{
return false;
}
// Make sure we have line of sight
HitActor = PawnOwner.Trace(HitLocation, HitNormal, AIOwner.Enemy.Location, PawnOwner.Location, true);
if ( HitActor != None && HitActor != AIOwner.Enemy )
{
return false;
}
return super.InternalCanDoSpecialMove();
}
function SpecialMoveStarted( bool bForced, name PrevMove )
{
super.SpecialMoveStarted( bForced, PrevMove );
if( AIOwner != none )
{
`AILog_Ext( self@"started for"@AIOwner, 'Husk', AIOwner );
AIOwner.AIZeroMovementVariables();
}
}
/** Turns the flamethrower on */
// why not change name?
// well I can, but the name is linked to the ANIMNOTIFY in the animation library
// and man I am lazy
simulated function TurnOnFlamethrower()
{
local KFPawn_ZedHusk HuskOwner;
HuskOwner = KFPawn_ZedHusk( PawnOwner );
if( HuskOwner == none || !HuskOwner.IsAliveAndWell() || bFlameThrowerActive )
{
return;
}
if( MyFlameSpray == none )
{
MyFlameSpray = HuskOwner.Spawn(FlameSprayArchetype.Class, HuskOwner,, HuskOwner.Location, HuskOwner.Rotation, FlameSprayArchetype, TRUE);
// Use a particular ImpactProjectileClass that will scale damage by difficulty
MyFlameSpray.ImpactProjectileClass = class'KFProj_HuskGroundFrost';
MyFlameSpray.OwningKFPawn = HuskOwner;
MyFlameSpray.SetBase(HuskOwner,, HuskOwner.Mesh, MyFlameSpray.SpraySocketName );
if( HuskOwner.WorldInfo.NetMode != NM_DedicatedServer && PSC_EndSpray != None )
{
if( PSC_EndSpray != None)
{
PSC_EndSpray.SetTemplate(MyFlameSpray.SprayEndEffect);
}
HuskOwner.Mesh.AttachComponentToSocket( PSC_EndSpray, MyFlameSpray.SpraySocketName );
}
if( HuskOwner.Role < ROLE_Authority )
{
// Set these to be visual only as we do the damage on the server versions
MyFlameSpray.bVisualOnly=true;
}
}
bFlameThrowerActive = true;
if( HuskOwner.Role == ROLE_Authority || HuskOwner.IsLocallyControlled() )
{
HuskOwner.SetWeaponAmbientSound(FlameAttackPlayEvent);
}
if( MyFlameSpray != none )
{
// Apply rally boost damage
MyFlameSpray.SprayDamage.X = HuskOwner.GetRallyBoostDamage( MyFlameSpray.default.SprayDamage.X );
MyFlameSpray.SprayDamage.Y = HuskOwner.GetRallyBoostDamage( MyFlameSpray.default.SprayDamage.Y );
// Start flames
MyFlameSpray.BeginSpray();
}
}
function SpecialMoveEnded(Name PrevMove, Name NextMove)
{
TurnOffFlamethrower();
super.SpecialMoveEnded( PrevMove, NextMove );
if( AIOwner != none )
{
`AILog_Ext( self@"ended for"@AIOwner, 'Husk', AIOwner );
}
}
/** Turns the flamethrower off */
simulated function TurnOffFlamethrower()
{
local KFPawn_ZedHusk HuskOwner;
HuskOwner = KFPawn_ZedHusk( PawnOwner );
if( HuskOwner == none || !bFlameThrowerActive )
{
return;
}
bFlameThrowerActive = false;
if( HuskOwner.Role == ROLE_Authority || HuskOwner.IsLocallyControlled() )
{
HuskOwner.SetWeaponAmbientSound(FlameAttackStopEvent);
}
// play end-of-firing poof. will stop itself.
if( PSC_EndSpray != None )
{
PSC_EndSpray.ActivateSystem();
}
if( MyFlameSpray != none )
{
MyFlameSpray.DetachAndFinish();
}
}
/**
* Can a new special move override this one before it is finished?
* This is only if CanDoSpecialMove() == TRUE && !bForce when starting it.
*/
function bool CanOverrideMoveWith( Name NewMove )
{
if ( bCanBeInterrupted && (NewMove == 'KFSM_Stunned' || NewMove == 'KFSM_Stumble' || NewMove == 'KFSM_Knockdown' || NewMove == 'KFSM_Frozen') )
{
return TRUE; // for NotifyAttackParried
}
return FALSE;
}
DefaultProperties
{
// SpecialMove
Handle=KFSM_Husk_FreezeThrowerAttack
bDisableSteering=false
bDisableMovement=true
bDisableTurnInPlace=true
bCanBeInterrupted=true
bUseCustomRotationRate=true
CustomRotationRate=(Pitch=66000,Yaw=100000,Roll=66000)
CustomTurnInPlaceAnimRate=2.f
// Animation
AnimName=Player_Flame
AnimStance=EAS_FullBody
// Flamethrower
FlameSprayArchetype=SprayActor_Flame'ZED_HuskFrozen_ARCH.Husk_Freezethrower_Freeze'
Begin Object Class=ParticleSystemComponent Name=FlameEndSpray0
bAutoActivate=FALSE
TickGroup=TG_PostUpdateWork
End Object
PSC_EndSpray=FlameEndSpray0
FlameAttackPlayEvent=AkEvent'WW_WEP_Cryo_Gun.Play_Cryo_Gun_3P_Start'
FlameAttackStopEvent=AkEvent'WW_WEP_Cryo_Gun.Play_Cryo_Gun_3P_Stop'
}
"Wow, vertical, you basically copied KFSM_Husk_FlameThrowerAttack.uc". Yup. I didn't really need to. Let this be a lesson for effeciency, as I could have easily extended KFSM_Husk_FlameThrowerAttack and simply simulated the TurnOnFlamethrower function. I did this instead because this was the last thing I needed to do, and I knew this worked. The issue with the base TurnOnFlamethrower is that:
MyFlameSpray.ImpactProjectileClass = class'KFProj_HuskGroundFire';
This class is not set in the defaultproperties, which means if I wanted to change the damage type, gatta change that function up. I thought to myself "well, whatever" and just copy pastad the whole code over. Definitely going to refactor this when I'm not exhausted of SDK.
Code:
class KFProj_Husk_Freezeball extends KFProj_Husk_Fireball;
DefaultProperties
{
// Grenade explosion light
Begin Object Name=ExplosionPointLight
LightColor=(R=128,G=200,B=255,A=255)
End Object
// explosion
Begin Object Name=ExploTemplate0
Damage=10
MyDamageType=class'KFDT_Husk_FreezeSuicide'
ExplosionEffects=KFImpactEffectInfo'ZED_HuskFrozen_ProjExp.FX_FreezeBall_Projectile_Explosion'
ExplosionSound=AkEvent'WW_WEP_Freeze_Grenade.Play_Freeze_Grenade_Shatter'
End Object
Begin Object Name=FlamePointLight
LightColor=(R=128,G=200,B=255,A=255)
End Object
Begin Object Name=ExploTemplate1
Damage=1
// Damage Effects
MyDamageType=class'KFDT_ZedFreeze'
ExplosionEffects=KFImpactEffectInfo'WEP_CryoGun_ARCH.GroundCryo_Impacts'
End Object
GroundFireExplosionActorClass=class'KFExplosion_GroundIce'
ProjFlightTemplate=ParticleSystem'KFP_CryoTrail.FX_Husk_projectile_frozen'
AmbientSoundPlayEvent=AkEvent'WW_WEP_SA_Crossbow.Play_Bolt_Fly_By'
AmbientSoundStopEvent=AkEvent'WW_WEP_SA_Crossbow.Stop_Bolt_Fly_By'
}
Thankfully, I had the consciounce to do this better.
Code:
class KFDT_Husk_FreezeSuicide extends KFDT_ZedFreeze;
static function PlayImpactHitEffects( KFPawn P, vector HitLocation, vector HitDirection, byte HitZoneIndex, optional Pawn HitInstigator )
{
local float ParamValue;
local int MICIndex;
MICIndex = 0;
if (P.GetCharacterInfo() != none)
{
MICIndex = P.GetCharacterInfo().GoreFXMICIdx;
}
// If we're dead and not already frozen (prevents re-shattering)
if ( P.bPlayedDeath
&& P.CharacterMICs.Length > MICIndex
&& P.CharacterMICs[MICIndex].GetScalarParameterValue('Scalar_Ice', ParamValue))
{
if (ParamValue == 0)
{
PlayShatter(P, false, `TimeSinceEx(P, P.TimeOfDeath) > 0.5f, HitDirection * default.KDeathVel);
return;
}
}
if(P !=None && P.Health>0 && P.AfflictionHandler!=None )
// accrue some freeze
P.AfflictionHandler.AccrueAffliction(AF_Freeze,60.f);
Super.PlayImpactHitEffects(P, HitLocation, HitDirection, HitZoneIndex, HitInstigator);
}
defaultproperties
{
KDeathVel=300
}
This is really the more important of the three, as it finally brings everything full circle. PlayImpactHitEffects is called when the projectile impacts an object. A lot of the above stuff is fluff, which shatters enemies or applies some scalar_ice paramater to the MIC. What's important is the AccrueAffliction. AF_Freeze is the affliction type asscociated with the freeze affliction, what we are doing is accruing 60 freeze_power to the AF_Freeze affliction type. because our human is vulnerable to 4X freeze, this will instantly overreach the threshold and wabam, our human is now locked in place for 2 seconds.
I'm going to end the guide here. I know I said I would talk about SDK effects, I will still do that, but probably as an addendum at anotehr time. This was a lot longer than I expected. post questions! Point out my Mistakes! HUZZAH!
Last edited: