AI Customization - Fire Elemental

From FleshWorks

Jump to: navigation, search

by Zillameth


Contents

Intro

This tutorial describes the process of adding a new AI type to Deadly Shadows' bestiary. An example of fire elemental is used, but the techniques shown here are useful for creation of any being (as long as desired characteristics are supported by game's engine). Not all topics are covered here, though. We are not going to add new meshes, textures nor animations. Instead, we will concentrate on using what's already available. This is still quite a bit.

This tutorial is for beginners. While it provides a complete recipe for a working fire elemental, its main purpose is to explain what pieces AI is made of.

Design

If you already have some experience with making your own modifications, this advice might sound like a truism, but it's so important it should be reiterated in every tutorial: never start anything by opening the editor. Instead, take a pen and a sheet of paper, have a seat, and try to answer a few questions (the sequence is not obligatory):

  1. What should your creature look like? Try to draw a sketch or write a short literary description (it doesn't need to be pretty, it's only for your personal use).
  2. What is its role in your mission or campaign? Is it an enemy, a shopkeeper or a bystander? Or maybe a Big Bad End of Level Boss?
  3. How would you describe it, in a few words? Is it dangerous? Mysterious? Scary? Funny? Supernatural? Is it smart or retarded? Does it rely on its intellect or feelings? Is it agressive or a coward? Is it good or evil (or maybe, like Garrett, is somewhere in between)?
  4. If it's an enemy, what are its advantages? In other words, what kind of danger does it bring to player's character?
  5. And how about weak spots? How does one defeat it? Or maybe one doesn't fight it in a regular way? Is creature neither too easy nor too difficult to get rid of?
  6. What makes it different from other creaures? Remember you already have whole setting at your disposal. If you just need a bunch of lawful, religious fanatics, then you probably don't need to design a new group. Hammerites will do just fine.
  7. Does it fit the setting? For instance, direct references to real places, people and events don't fit at all. On the other hand, logical extrapolations of "canon" are usually safe.

These are important questions and they force you to make important decisions, but don't get too used to them. During implementation, it may turn out that something doesn't work as planned, or at all. Many things are not supported by the engine, and some require too much effort. Although, in general, specification (that is, answering above questions) should be separated from implementation (that's what the rest of this tutorial is about), it may be beneficial to take some technical limitations into account during design process. For example, if you know you don't have means to create a new mesh, then you should probably assume your creature is a humanoid.

Let's apply the above to our custom AI. We have already decided it would be a fire elemental, and now we need to fill in details.

  1. As a fire elemental, it should, ouf course, be made exclusively of fire. Because we are, indeed, not going to use a custom mesh, we assume our elemenal has a vaguely humanoidal shape. Because of its nature, it casts bright, orange light and emits cracking, "fiery" sounds. It makes additional sounds when it's angry or notices something unusual. When elemental enters combat, it makes a loud "announcement" and becomes more noisy in general. During fight it gives more light than usual, which is a sign of agitation.
  2. Fire elementals are not common in the City. They are violent and don't like to negotiate, so allying with them is unlikely. They aren't sentient nor organized, but they are more powerful than most creatures, so there are not going to be many of them. We want player to fight them one by one. Fire elemental will be a good guardian of treasure or important landmark (such as the only bridge across an underground river of lava).
  3. Agressive and mindless. They are frightening, because they can burn a human merely by standing next to him, but they are not inexplicable. They are unusual creatures, so they are connected to unusual matters. They portend presence of something magical and definitely valuable (to someone).
  4. Fire elemental attacks with its "limbs", but what really makes it dangerous, is its temperature. It doesn't need to hit Garrett to hurt him, it just needs to get close. It has its own source of light, so it's quite difficult to hide from it.
  5. Fire elemental is invulnerable to most weapons, and cannot be knocked out, but can be harmed with water, and water arrows in particular. This, and its ability to throw light upon its surroundings, means that player is going to fight it from distance. We will make elemental slower than most creatures, in order to allow player to outmaneuver it.
  6. Our new AI has a completely different set of vulnerabilities than humans and requires a different kind of stealth (a "find a good spot and kill me with sniper shot" kind, rather than "find a suitable route and sneak by me" kind). It also fits different scenography: nobody would expect an ordinary human guard in a forgotten, ruined city of the ancients.
  7. Fire elementals made appearance in the first installment of Thief, The Dark Project. They don't need to fit into canon, because they are part of it.

Okay, now you can put pen and paper away and open the editor. You might want to stop reading for a moment and build a small test mission. It should contain one room with Player Start, one large, partially lit chamber, and two corridors connecting them: one short and well lit, and one dark and winding.


Data Structure

Having lots of AIs is not enough. We need to be able to manage them. Hence, we need to use data structure. Data structure is pretty much like table of contents or index in a book. Normally you don't think much about tables of contents when you read a novel, but imagine how would trying to find description of council meeting in Rivendell look like if "The Lord of the Rings" wasn't divided into chapters!

Open Actor Class Browser (available through View menu). On the left there is a tree containing names of all available classes. Each class defines exactly one kind of object and each object you create belongs to exactly one class. Click any name. On the right you can see list of all properties of chosen class (and if you can't, there should be a sign "Properties" in the lowest row with a [+] next to it - click that [+]). All objects of this class, when you create them, will have exactly the same properties as the class they belong to. You can change any property afterwards and even add new ones, but you can't remove those which already exist (if you do so, your object may appear to have lost them, but in fact they will remain there, hidden, unless you check "Show non-Local Props" box in object properties window).

Maybe you're wondering, why there is a tree of classes, and not a simple list. This is because some classes are based on others. For example, class Pawn is based on class Actor. We say Actor is the base (or "parent") class of Pawn, and Pawn is a child class of Actor (we also say that Pawn "inherits" from Actor). The relation between base class and child class is similar to relation between class and object. Whenever you create a new class, it starts as an exact copy of its parent class. You can then (and even should!) change some of its properties, or add new ones. You should always try to choose a base class, which is as similar as possible to the class you're going to create. For example, if you want to make a hand torch, which gives purple light, it's best to look for a regular hand torch, create a child of it, and only change light color to purple. This will save you time and keep your data structure tidy (which is really important in the long run).

You don't have to create new class every time you need a new kind of object. If you only need one purple hand torch and don't plan to use more in future, just create a regular torch and change properties of the object you created. Rememeber, though, that changes made to objects apply only to these objects, while changing a class means changing every object of that class. If you plan to use lots of purple torches, it's more efficient to create a new class, and thus avoid modifying every new torch manually.

Awright, let's create a class for our fire elemental! All AI classes in Deadly Shadows are based on T3AIPawn, which is based on AIPawn, which in turn is based on Pawn. Take a look at all its children (and their children, and so on). Their names are, in most cases, self explanatory. This is a good practice, because if you are looking, for example, for a torch class, you don't have to analyze all classes' properties; instead, you only need to find a class named "torch". We are particularly interested with T3AIPawnCreature, which is parent class of all non-human AIs. This is where our fire elemental fits best. Right click T3AIPawnCreature and choose "New". Enter "FireElemental" instead of "MyT3AIPawnCreature". You can, of course, choose a different name, but keep in mind that some characters are forbidden (spaces in particular). It is also best to choose a name in English, which is today widely accepted as the language of computer science. Without context, you would never guess what a "ZywiolakOgnia" is, would you?

Before we forget about it, let's right click your new class and choose "Placeable/UnPlaceable". An asterisk will appear next to class name, to indicate that from now on editor will allow you to place objects of FireElemental in your maps. Don't do that yet, though. Your class doesn't have all the properties required. We will take care of that in just a moment.


Properties

Properties store a huge part of information about classes and objects. Some of them reperesent object's current state and change during game. Others describe object's initial setup. Take a look at FireElemental's properties. It has a property category called "Health", and in that category - two properties: HealthState and MainHealth. They already have their values set. MainHealth is printed in pale blue, which means FireElemental doesn't have its own version of this property and uses its parent's version instead. If you change any field of MainHealth, its color will change to indicate that its value has been set locally.

Don't let the number of properties intimidate you. It's all very logical an consistent. The best ways to learn what each property means are:

  • read its name (seriously, there are many who suffered, because they forgot about this option),
  • read developers' comments,
  • browse existing classes,
  • take a closer look at original missions and see how they work,
  • perform lots of your own experiments (don't worry, as long as you work on objects instead of classes, and don't delete anything, you can't do any permanent damage).

In this tutorial, we are only going to change and add a few properties. We will start with Actor -> VoiceTag This property describes, which set of voice responses AI is going to use. The value FireElemental inherited from T3AIPawnCreature is "T3CityGuard". That won't do, because we don't want fire elemental to sound like a human, and we definitely don't want it to rant about boots being too tight or captain knowing nothing. In fact, none of voices shipped with Deadly Shadows will do, because none of creatures from the original campaign make noises, that could be described as "fiery". We are not going to create our own voice either. We will take care of this problem later. For now, set Actior -> Voice Tag to null (that is: leave the field empty, so that it doesn't contain any characters at all). This will inform the game, that we don't want it to give our AI any voice.

Tip: if you like voice of one of the creatures from original missions, and wish to give it to your AI, you can find this creature's class in T3AIPawn tree, copy Actor -> VoiceTag value and paste it to your own AI's Actor -> VoiceTag. The same method can be applied in almost any case, as long as you know which property to look for. Some behaviours are described by more than one property.

Sound -> CharacterType Sound -> SoundAmbient


FireElemental class doesn't have these properties, so we need to add them manually. Right click any property category and choose "Add Property". You should see a window with a list of categories on the left. Choose Sound from the list. Another list shows up on the right - these are properties assigned to Sound category. Choose CharacterType and SoundAmbient (you can make a multiple selection by left clicking while holding Control key). Click "Add Property". Both should appear on property list of FireElemental, in category Sound.

CharacterType determines footstep sounds. We don't want fire elemental to make any, because it doesn't have feet, so set this property to null. What we do want, though, is to hear sound of fire in elemental's proximity. Choose SoundAmbient and click button with three dots. Now go make yourself a cup of coffee, for "Schemas" is a set of over fifteen thousands of short text files, and loading them is going to keep your computer busy for a minute or two. Once this grossly inefficient procedure is behind us, you should see a new window. This is the sound schema browser. Countrary to poor first impression, sound schemas are a clever invention. Each of them describes a way, in which a sound (or set of sounds) should be played. Therefore, "playing a schema" is almost the same as "playing a sound", even though schemas are just simple text scripts (so simple, in fact, that we are going to make our own by the end of this tutorial).

Go to schema browser, open sounds directory and then schemas_sfx. Find fireplace_lp and select it. Minimize browser, and return to your elemental's properties window. Find Sound -> SoundAmbient again, but this time click "Use". Property value should change to "fireplace_lp".


AI -> AnimationSpeedMultiplier As we stated before, we want to slow elemental down. We will set this property to the same value zombies use: 0.65. This means fire elemental will be 35% slower than any creature, whose AI -> AnimationSpeedMultiplier is set to 1 (city guards are among them). Garrett should be able to escape from it with no trouble.

AIPawn -> CombatModelClass
AIPawn -> MovementModelClass
AIPawn -> BehaviorModelClass
AIPawn -> FactionModelClass


All properties in AIPawn category (with the exception os DiagnosticsOn) point to other classes. These can be found in Actor -> MetaProperty -> AIModel tree. Let's take a look at one of combat model classes, called ZombieCombatModel. It has three interesting property categories: AICombat, AIMeleeCombat and AIRangedCombat. They all parametrize AI's behaviour in combat. You can always create a new class and set its properties to any values you like (within reason). The same applies to all model classes. Actually, we are going to make one for our elemental right now, because we forgot about a minor detail: fire elementals should not slip on oil puddles. Hence we cannot use one of existing bahaviour models, because, in general, all creatures with legs are vulnerable to these slippery yellow bastards.

Find T3HumanBehaviorModel and create its child. Call it FireElementalBehaviorModel. Take a look at its properties. One of categories is called AIStateFlags and contains a lot of properties, whose names begin with "StateEnabled". The number of states is finite and AI is always in one of them. Its whole behaviour is internally described as moving between these states. Usually, there is a state for each possible action (and if there is not, it means AI cannot perform this action without doing something else, or at all). Sadly, AI knows nothing about slimy nature of oily liquids, so when we set StateEnabled_Slipping to 0, it will be unable to enter "slippng" state and will become effectively invulnerable to oil. Apply this change now.

Let's go back to the FireElemental class and its AIPawn properties. Set BehaviorModelClass to FireElementalBehaviorModel. As for combat, we want elemental to be agressive and mindless, just like zombies. Set CombatModelClass to ZombieCombatModel, FactionModelClass to UndeadFactionModel and SensoryModelClass to UndeadSensoryModel.

There is one more model class property in this category: MovementModelClass. Its value is a matter of taste, because it determines, which set of animations your AI will use. For example, if you choose ZombieMovementModel, your elemental will have stiff arms, and if you choose StatueMovementModel, it will appear more muscular and threatening. It might be difficult to notice the difference, though, because...

Render -> SkeletalTag
Render -> MaterialSkin
Render -> CastShadows
Render -> Transparency

...we are going to make our AI invisible. More precisely, its mesh will be invisible. Fire elementals need to have a mesh, because without it they would be unable to move around your level. On the other hand, no combination of mesh (the geometry) and skin (the textures) looks like a fire elemental. Our solution is to have them there but convince the engine not to draw them. We will achieve this goal by adding Render -> Transparency property and setting it to 1. Add Render -> CastShadows, too, and set it to ShadowCastingFalse. Quite obvoiusly, fire elemental will not cast a shadow. Add Render -> SkeletalTag and set it to GuardSkinny. The last property to set is Render -> MaterialSkin. Its value is unimportant, because elemental's body is invisible, but we have already taken a few things from the undead, so we might as well use one of their skins (such as Zombie_01).


Links

We are going to perform a simple trick: elemental's body is invisible, but we will attach visible flames to it. When elemental moves, flames will follow its motions. From the player's point of view it will appear as if elemental was made exclusively of fire.

We need to use links. Link is a relation between two objects. When one of these objects changes, the other may change too, in a way, which depends on link's type (also called "flavor"). For example, RigidAttachment link causes one object to follow another in space. Linking a human to an item with RigidAttachment makes it all look as if item was carried by human. The most obvious example of this technique is a torch bearing guard, but closer inspection reveals that many AIs use RigidAttachment links to carry pieces of clothing, hair and even their teeth and eyes.

Links refer to objects, but you can assign them to classes, too. When an object of a linked class is created, all objects needed to fulfill all links are spawned automatically. For example, GenWallTorch class has links to two other classes. Every time you place a wall torch on your map, editor automatically adds flame and smoke, even though they are two separate objects.

Note: children classes inherit links from their parents. If you make a child class of GenWallTorch, it will have the same two links, even though they will not appear on the list.

Find FireElemental class in actor browser, right click it and choose "Show Links". With no surprise to us, our AI doesn't have any. Click "Add Link". You should see a window with a long list of link types. Choose RigidAttachment. There are two more fields, and one of them is already filled with the name you chose for your class. We need to fill in second field too, but we can't choose name from a list, unfortunately. Click the empty field and type "TorchFlame". This is the name of class of emitter, which produces visual effect of... torch flame, of course.

Note: emitters are relatively complex objects with simple purpose: they constantly spawn sprites. Sprites are basically small, flat pictures. Emitters control their sizes, colours, texturing, lifetimes and movement. They can be used to produce flame, smoke, sparks, rain, snow, etc. Such effects add much spectacularity, but can be quite resource consuming. Use them with care.

Click OK. Now we have a single link on our list, but it isn't fully configured yet. Select it and click "Edit Selected Link". As you can see, links have properties too. Choose Attachment -> m_parentBone and click button with three dots. A list of strange strings should appear. That's what we needed a mesh for. Each AI mesh has a set of predefined "hardpoints", to which other objects can be attached. We are interested in points, whose names begin with "HP" and "M 1". As their names clearly suggest, they are related to body parts. Choose HP_LeftHand and click OK. From now on, each of your fire elementals will have a torch flame attached to its (invisible) left hand. Don't close the link data window yet. Find Link -> m_name property and set it to "Flames". This is a custom name, you can choose any string. Write it down, though, because we are going to need it, when we get to writing scripts. You can now click "Done". Congratulations, you have set up a link!

How about another one? Repeat above actions, but each time choose different hardpoint:

  • HP_RightHand
  • HP_LeftHip
  • HP_RightHip
  • HP_RightKnee
  • HP_LeftKnee
  • M 1 L_Toe0
  • M 1 R_Toe0


And that's not all. Create two more links, but this time use "FireplaceFlame" instead of "TorchFlame". Set m_parentBone to:

  • HP_FrontWaist
  • HP_Head

There are two more things we need to do with links. Click "Add Link" again. Choose Vulnerability and type "FireVulnerability" into TO field. Click "OK", then "Done". FireVulnerability is a child of MetaProperty -> VulnerabilityObject and defines, which kinds of damage an object is vulnerable to. If we don't link our elemental to any VunerabilityObject, it will become indestructible. Since our elemental is made of fire, it seems reasonable, that should use the same vulnerabilities as fire. You can, of course, define your own child of VulnerabilityObject, and define, for example, a heavily armoured guard with 50% resistance to physical attacks.

Time for the last link. We've planned to equip elemental with its own source of light, haven't we? Switch to actor browser and go to Actor -> WorldObj -> Lights_ -> FlameLight -> TorchLight. Create child of TorchLight and name it "InvisibleHandTorch". Make it placeable. Delete all scripts and set Scripts -> bEnableInheritScripts to False (torches are controlled with scripts, but we don't want any special controls, because we only need a dumb, torch-like source of light). Change RenderType -> DrawType to DT_Sprite to inform editor, that this object doesn't have a mesh. Now return to FireElemental class and open Links window again (right click, "Show Links"). Add RigidAttachment link FROM FireElemental TO InvisibleHandTorch. Set its m_parentBone property to HP_LeftHand and m_name to "Light Source" (just like in case of "Flames", this is a custom name).

One more step, and our fire elemental will be ready for first test drive.


Weapon Loadout

Game development is an art of illusion. Did you know that Pagan will-o-wisps in Deadly Shadows are not AIs at all? They are elevators. If such things can happen, then it shouldn't be a surprise, that we need to assign a weapon to fire elemental, even though it doesn't use any.

It's time for another small modification. Switch to actor class browser and find Actor -> WorldObj -> SetDressing -> AIParts -> AIFakeMeleeWeapon. This is the object an AI is given, when it fights with bare limbs. It's almost perfect for our purposes, but when AI hits something, it makes completely "non-fiery" sounds. Create its child, and call it "AIInaudibleFakeMeleeWeapon". Set its PhysicsSound -> PhysSoundTag property to null, and Weapon -> WeaponStimulus to StimulusType_Fire. This fake weapon will now make no sounds and deal fire damage.

Find FireElemental, right click its name, and choose "Edit Weapon Loadout". Select LeftHip. Click "Make Local". Click "Add Item to Slot". You will see a subset of actor class tree - these are objects, which can be placed on AI's body. They all have one thing in common: the Weapon -> WeaponStance property. Its name is a little unintuitive, because it also handles important personal belongings, such as purses and keys (such items should have WeaponStance set to eWS_ITEM). Choose AIInaudibleFakeMeleeWeapon (it should be right at the bottom of the tree). Click OK and "Done".


Test Drive

If nothing went wrong, the elemental should be functional now. Put it somewhere on your test map. It would be best to place it in a spot, where you can both aproach it openly, and sneak behind it.

Note: Editor will ask you if you want to save custom gamesys. We have modified gamesys, when we added our custom classes, so click "yes". Editor will create a backup of previous gamesys automatically.

Something is amiss. The elemental is too silent, and light doesn't change brightness at all. If you kill elemental with water arrows, it will stay still after death, instead of doing something appropriately spectacular. Remove it from your map. We need to enrich our creation with a few scripts.


Scripting

Script is a seqence of commands. You can use them to merge simple actions, such as turning a light on or sending an AI to specific location, into complex behaviours. More importantly, though, scripts also contain conditions, which must be fulfilled before actions are performed. This means you can use scripts to teach AIs how to react to events.

Open script browser by clicking "Trigger Scripts" in View menu. On the right there is a tree of script names. Names denoted with book icons are groups. It doesn't matter for a script, which group it's put in, but it helps you find it quickly. You can, for example, create a "MyScripts" group and put all your scripts in there. Or you can create a separate group for every mission or AI you make. A group called "Test" is a good place for experimental scripts you don't want to use in real missions.

Click "New". A window with six buttons and two fields opens. The smaller field contains script name. It is currently set to rather boring "DefaultName". Change it to "FireElementalChangeState". You can use a different name, but as always, it's best to choose something that describes script's functionality. This one will be executed every time fire elemental goes from one AI state to another.

The game's engine keeps track of all important events that occur in your map and detects when some of them fulfill one or more script conditions. When that happens, all actions of an appropriate script are executed. Your task is to define, WHEN script execution should start, and WHAT should be done. Hence two kinds of commands: conditions (when), and actions (what).

Click "Condition". As you can see, there are many, many available conditons. You cannot create new ones, because it would require modification of game's executable files, but that's okay, because with so many predefined conditions there are few things you can't do (and most of them aren't supported by the engine anyway). We will not have them explained here, because this is a topic for a book, and their names make their function pretty obvious anyway. Take some time to browse the tree and see, what options you have.

Find group called "AI" (it should be right on the top), open it and select Image:Condition_icon.png When my behavior state changes to [Enum:EStateType]. When it says "me", it means the object script is attached to. In general, scipts are always executed in context of their posessors. You can only refer to another object in your script, when the object you attached it to and the other object are linked together (with one exception, which will be explained in a few moments). Strings in square brackets, such as "[Enum:EStateType]", are variables you need to set manually. If you have already selected the aforementioned condition, click "OK". It will be added to the condition list of your script. The string "[Enum:EStateType]" should be printed in light blue. Click it. A list of correct values of this variable should be displayed. In this case, each value refers to one of states AI can enter. Note that we have previously made it impossible for fire elemental to enter "slipping" state, so if you choose STATE_SLIP_AND_FALL, this condition will never be fulfilled, and script wil never execute its actions. Choose ANY instead. Condition should now have the form: "When my behavior state changes to [ANY]". It will be fulfilled, when the object changes its state to any other.

We can any number of conditions, but this script only needs one, so click "Action" button. Actions are organized in the same way as conditions. Open "Sounds" group and select Image:action_icon.png Play sound schema [SoundSchema] at my location. Click "OK". Click [SoundSchema] - this will open sound schema browser. Find "fire_short_burst", select it and minimize the browser. [SoundSchema] should change to [fire_short_burts]. If it doesn't, click it and choose fire_short_burst form the schema browser again.

The action you have just added does exactly what it says it does: it plays a sound schema. Note: this is so-called "non-looping" schema. It only causes sound to be played once. There are also looping schemas. They are played indefinately, unless interrupted by event (such as end of game) or script. The easiest means to recognize looping schemas are their names. Usually they end with "_lp" suffix (this doesn't apply to ambient sounds used as background "music" in original missions).

There is one problem with our script: it will be executed only once. This is a general rule: if we want script to be executed multiple times, we have to say so. Click "Action" again, but this time open "Scripts" and select the last action in this category: Image:action_icon.png Reset script conditions and actions. Click OK. When script execution process reaches this action, it will return to script's start and wait until conditions are fulfilled again. Note: the sequence of actions is significant. If you add "Reset..." before "Play [SoundSchema]...", your script will never reach the latter. You can always click and drag action or condition to change its sequence. You cannot mix conditions and actions together, though.

Your script now has a name, one condition and two actions. That's enough. Close the window by clicking "OK". If you wish to change it in the future, you can always select it from the browser and click "Edit". Your script is almost ready for use. We still need to attach it to FireElemental class, but we will do that after we make a few more scripts.

FireElementalAngry

In the first section of this tutorial, we decided we wanted elemental to make additional sounds, become more noisy and give more light, when it enters combat. We will take care of that now. Create new script and name it "FireElementalAngry". Add condition Image:Condition_icon.png when linked AI [LinkFlavor] enters combat. Click [LinkFlavor]. We have already seen a similar list, haven't we? These are the same link flavors we chose from, when we attached flames to elemental's mesh. There are some entries we haven't seen yet. For every link name there is its "copy" with tilde at the beginning. These are so-called "return links". When you create an ordinary link from object A to object B, a return link from B to A is created automatically. They don't do anything on their own, but give the engine (and you) easier access to data. There are also two special cases of flavors: MYSELF and PLAYER. These are not really links. MYSELF always points to the object script is attached to, and PLAYER always points to Garrett (hence you can always refer to him, even if there is no link between him and your object). Choose MYSELF and click "OK". You contidion should now say: "when linked AI [MYSELF] enters combat".

Add three actions Image:action_icon.png Play sound schema [SoundSchema] at my location, and assign three schemas to them: furnaceflameburst, hit_arrowfire_lo, and furnaceflame_lp. The first two are non-looping schemas. When mixed, they sound like a terrifying, fierce roar. The last schema refers to a looping sound of furnace flame, which is louder than fireplace flame. It will be played by fire elemental until we tell it to stop.

Add Image:action_icon.png Set [Property] (Int/Float/Byte property) to [Value] on linked objects of [LinkFlavor], changing it over period of [Float] seconds action from "Properties" category. This one is very useful in a variety of situations, because it allows you to change object properties dynamically. We have already attached an invisible torch to our elemental. Now we will change its light range. Our script will be attached to fire elemental, to which torch is linked through RigidAttachment. Click [LinkFlavor]. Choose RigidAttachment from the list, but don't close the window. There is a text field at its bottom. Remember when we created links between elemental and torch and set m_name to "Light Source"? If we didn't do that, there would be no way to tell this RigidAttachment link from the others, and our script would apply property change to all objects linked through RigidAttachment (including flames). Click the text field and enter "Light Source" (or the value you assigned to m_name, if you chose a different one). Click OK. Instead of [LinkFlavor] there should be now [RigidAttachment:Light Source]. Set other values of this action to LighShape.LightRadius, 64 and 1.0, respectively. 64 is twice the usual range of a torch light. We want it to change it over a period of 1 second, because changing it instantly would look ugly. Add Image:action_icon.png Reset conditions and actions, and make sure it's at the end of script. Click "OK" to return to script browser.


FireElementalEndCombat

This script is the reverse of FireElementalAngry. Try to figure out by yourself, how it works.

FireElementalEndCombat

Image:Condition_icon.png CONDITIONS

Image:AND_icon.png Logic AND operator
Image:Condition_icon.png When linked AI [MYSELF] exits combat
Image:Condition_icon.png Query if [HealthState] is [Equal_To] [HealthState_Alive] on any linked objects of [MYSELF].

Image:action_icon.png ACTIONS

Image:action_icon.png Stop playing [furnaceflame_lp] on my object.
Image:action_icon.png Play sound schema [furnaceflame_ext] at my location.
Image:action_icon.png Set [LightShape.LightRadius] (Int/float/Byte property) to [32] on linked objects of [RigidAttachment:Light Source], changing it over period of [5.00] seconds.
Image:action_icon.png Reset script conditions and actions.


In case you wonder, why we check if AI is alive: that's because AI's death is a special case of combat exit, and we want to handle it separately.


FireElementalKilled

In Dark Project, when a fire elemental was hit with a water arrow, both disappared, as if elemental was suddenly extinguished. We will extinguish our elemental too. Then we will destroy its invisible body, so that it doesn't make a frustrating obstacle to player and other AIs.


FireElementalKilled

Image:Condition_icon.png CONDITIONS

Image:Condition_icon.png When my health state changes to [HealthState_Dead] from [ANY].

Image:action_icon.png ACTIONS

Image:action_icon.png Set [LightShape.LightRadius] (Int/float/Byte property) to [0] on linked objects of [RigidAttachment:Light Source], changing it over period of [2.0] seconds.
Image:action_icon.png Finish playing the [fireplace_lp] quickly (don't finish current loop) on my object.
Image:action_icon.png Finish playing the [furnaceflame_lp] quickly (don't finish current loop) on my object.
Image:action_icon.png Play sound schema [furnaceflame_ext] at my location.
Image:action_icon.png Delay [1.00] GAME seconds.
Image:action_icon.png Set [EmitterSpawnState] to [EDS_NoNewPartices] on linked objects of [RigidAttachment:Flames].
Image:action_icon.png Delay [2.00] GAME seconds.
Image:action_icon.png Destroy linked object(s) [RigidAttachment].
Image:action_icon.png Destroy linked object(s) [MYSELF].


The last two actions destroy the elemental, as well as flames and torch. We don't need to specify link name, because we want to destroy all objects linked through RigidAttachment (both torch and flames). When we destroy elemental, all sounds played on it are stopped, so we need to make script wait, until furnace_ext shcema ends (hence Delay actions). Setting EmitterSpawnState to EDS_NoNewParticles means our flames will stop producing new sprites, but will not delete existing ones, until they are past their lifetime. We don't have to add "Reset script conditions and actions" at the end, because every fire elemental dies only once.


FireElementalHit

This is the last script we need to make. Its only tricky part is delay. It means that script must wait for two seconds before starting from the beginning. It prevents game from playing multiple copies of schemas, when elemental is hit by several enemies at the same time.


FireElementalHit

Image:Condition_icon.png CONDITIONS

Image:Condition_icon.png When I take health damage [Greater_Than] [0.00] from [ANY] at [ANY].

Image:action_icon.png ACTIONS

Image:action_icon.png Play sound schema [furnaceflameburst] at my location.
Image:action_icon.png Play sound schema [hit_arrowfire_lo] at my location.
Image:action_icon.png Delay [2.00] GAME seconds.
Image:action_icon.png Reset scripts conditions and actions.


That's almost all. Return to script browser, click "OK", and go make another cup of coffee. You have to click "OK" every time you make or change a script and want editor to notice this change. Avoid closing the browser in other situations, and minimize it instead. When your editor is done, find FireElemental in actor class browser and add property Scripts -> TriggerScripts to its properties. Click "Add", then click the [+] next to "TriggerScripts", then "none", and finally the button with three dots. Select any of your FireElemental scripts in Script Browser, then click "Use" in Actor Class Browser. "None" should change to the name of selected script. You have just attached this script to your class. Do the same for the rest of scripts you made.

Our fire elemental is almost complete. Put it on your test map. Add some water arrows too. Try to anger the elemental, run away from it, and kill it with water arrows. This will allow you to check whether all scripts work correctly.


Sound Schemas

One minor, but irritating details remains: sound volumes are all wrong. You can only hear fireplace flame, when you get really close, but flame bursts are so noisy you can probably hear them from any point of your test map. This is schemas' fault. One of their functions is to define distance, from which a given sound is audible. Open any system exploration utility and open directory, in which you keep your copy of Thief (the one you use for editing). Open CONTENT\T3\Sounds\. The schemas you saw in schema browser are actually text files, and they are all kept here. Open schemas_sfx directory and find fireplace_lp.sch. Do you recognize the name? It's our fireplace schema. Create a copy of it and name it fireplace_alt_lp.sch. The other schema we need is fire_short_burst.sch. Create its copy too, and name it fire_short_burst_alt.sch. Now open both copies in any text editor, and find entry called "radii". It has two parameters, separated with commas:

  • 1 and 25 in case of fireplace_alt_lp,
  • 1 and 150 in case of fire_short_burst_alt.


First parameter in each entry is maximum distance, at which sound is played with maximum volume. Second parameter is total range of sound. Notice that fire burst has six times as long range as fireplace. Change both 25 and 150 to 50. Save both files (make sure they still have .sch extension), and return to T3Ed. Open sound schema browser and see if your new schemas are on the list. If they are not, save everything and restart the editor.

The last step is to replace old schemas with their modified versions in your elemental's properties and scripts. You need to change Sounds -> SoundAmbient property, as well as two scripts: FireElementalChangeState and FireElemenalKilled.


To be continued

This tutorial ends here, but it doesn't mean you cannot make your elemental even better. For example, you can use different emitters and sounds to create different kinds of elementals. You can make your AI spawn a fire arrow upon death. You can delete links between elemental's legs and flames, to make it appear as if it was floating about one metre above ground. You can modify combat settings and other properties - maybe a single water arrow should be enough to kill the monster? Don't be afraid to experiment. That's the best way to learn how it all works.



Back to Mission Design: AI Tutorials Page

Back to Mission Design Tutorials Page

Back to Main Tutorials Page

Personal tools