Happy New Year, or near enough.
Over the holidays I've had some free time to finally learn FPI scripting and set out to solve the one problem that has plagued FPSC since the very beginning: Pathfinding.
Over 10 years too late for it to be relevant but whatever
I looked for the simplest solution for non dark ai scripting and settled on the Doom engine solution: Move towards the player, if you can't move towards the player then turn in a random direction and move there
After I got this kind of working I decided to recreate the basic routine of Doom's AI in FPI to see if it was possible.
First I had to set up a tic system as AI in Doom was checked every frame at 35fps, as well as the pseudo random number generator.
Then I had to sort out the alerting from sight, sound noise and touch, including the ability for other monsters to alert each other.
Then I had to sort out the movement routine, including a somewhat accurate recreation of the checks done to perform attacks.
Then there was the redirection and 'pathfinding', which I chose to make using raycast to detect wall collisions as I couldn't figure out how to detect a character's momentum.
Finally the was the painstates that pause the enemy AI semi randomly while taking damage.
After that I decided to recreate the attack patterns of some of the monsters.
This was somewhat simple but I kept running into inexplicable animation problems and needed to tweak some things to work better.
I assume my scripting style is trashy and messy but I tried to stay loyal to the source material as closely as I could as someone only vaguely familiar with both C and FPI
I tried to annotate it the best I could.
Hopefully these scripts allow people to create simple doom clones in the engine. Use these with projectiles without slash damage and it's quite engaging.
Really this should have been the first thing anyone should have tried to make in the engine. The good old 'Can it Run Doom?' question.
This was a project in nostalgia for 2 of the most influential things to me, as well as a test of the limitations of this approachable but infamously limited tool for amateur FPS designers.
I wanted to find where the FPI scripting succeeded and failed by emulating the AI of a simple and classic game written in a higher level language (C)
I found inexplicable problems, such as the slow ticrate and inconsistent P_RANDOM checks as well problems with animations that worked perfectly fine in stock scripts.
I thought about trying to make it using only V1.0 scripting however I knew that would not be possible at all so I chose to use the current standard of scripting to see how effective it ended up.
So here's the end result:
VERSION 1 DOWNLOAD
VERSION 2 DOWNLOAD
VERSION 3 DOWNLOAD
Appear Scripts
Variable are used to define the AI's attributes and animations, allowing there to be only one Main script that is neccessary.
By changing these variables you can affect character speed, strength and such in a reliable manner.
Since a lot of the attack behaviours are defined by animation timing it might be useful to select useful animations for such things.
Here are some default values. They're based on Doom engine values but get adjusted to FPSC scale in the mainscript (speed 8 translate to 100)
:state=0:dimlocalvar=MELEERANGE,setvarrnd=MELEERANGE 64 ;//default melee range
:state=0:dimlocalvar=spawnstate,setvar=spawnstate 1 ;//idle animation
:state=0:dimlocalvar=spawnhealth,setvar=spawnhealth 100 ;//max health
:state=0:dimlocalvar=seestate,setvar=seestate 2 ;//walk animation
:state=0:dimlocalvar=seesound,setvar=seesound 0 ;//sight sound
:state=0:dimlocalvar=reactiontime,setvar=reactiontime 8 ;//reaction time before first possible attack
:state=0:dimlocalvar=attacksound,setvar=attacksound 0 ;//attack sound
:state=0:dimlocalvar=painstate,setvar=painstate 1 ;//pain animation
:state=0:dimlocalvar=painchance,setvar=painchance 200 ;//pain chance
:state=0:dimlocalvar=painsound,setvar=painsound 0 ;//pain sound
:state=0:dimlocalvar=meleestate,setvar=meleestate 6 ;//melee animation
:state=0:dimlocalvar=missilestate,setvar=missilestate 1 ;//missile animation
:state=0:dimlocalvar=deathstate,setvar=deathstate 15 ;//death animation
:state=0:dimlocalvar=xdeathstate,setvar=xdeathstate 15 ;//gib death animation
:state=0:dimlocalvar=deathsound,setvar=deathsound 0 ;//death sound
:state=0:dimlocalvar=speed,setvar=speed 8 ;//speed
:state=0:dimlocalvar=radius,setvar=radius 20 ;//radius
:state=0:dimlocalvar=height,setvar=height 56 ;//height
:state=0:dimlocalvar=mass,setvar=mass 1000 ;//mass
:state=0:dimlocalvar=damage,setvar=damage 0 ;//damage
:state=0:dimlocalvar=activesound,setvar=activesound 0 ;//active sound
:state=0:dimlocalvar=flags,setvar=flags 0 ;//flags
:state=0:dimlocalvar=raisestate,setvar=raisestate 24 ;//revival animation
:state=0:dimlocalvar=refire,setvar=refire 40 ;//full auto fire speed?
:state=0:setalphafade=100,runfpidefault=1
Main Script: MainAI.fpi
This handles all movement and decision making of the monsters. All monsters should use this or a variant as their main AI loop.
They do not use waypoints for anything as that was a Quake feature.
Enemies alert, move, attack and redirect with the same method as the 1993 engine. They can open closed doors (and maybe switches?)
Sadly infighting is not possible as the target mechanics of FPSC are not reliable for targeting anything other than players.
It uses a tic system to emulate the pace of the original AI.
If you want an ambient sound put one in your character's sound1 field. It will randomly play while the AI moves.
These lines have sounds you can change.
:state=2:sound=audiobank\voices\monster\spawn.wav
:state=101:setframe=%painstate,sound=audiobank\voices\monster\hurt1.wav,state=111,bloodspurt=1
If you want the AI to respond faster or attack more you can change the line
:varequal=gametic 0:setvar=gametic 14 ;//29 seems really slow, try half instead
If you lower that value it updates the AI more often (this will cause the AI to move less far however as the movecount will decrease faster, but as a result the likeliness of attacking is higher
As you can see I lowered it be default as the tic system seems oddly slow.
If you want the AI to move for longer distance change the line
:state=51:setvar=flags 0,setvarrnd=movecount 15,state=4 ;//go to A_Chase with up to 15 steps to make
As you can see this by default chooses a random value for the movecount between 0 and 15. (keep in mind AI can only attack while movecount is less than 1)
7 Shoot Scripts
These affect how an AI attacks and are roughly based on the attacks found in the original games.
Projectiles and guns should use different scripts because I intended to make the guns have delayed aiming, however that is not possible in FPSC (though it is possible for projectiles?). The gun scripts set their weapon accuracy to 20% instead.
Damage is randomised between 60-140% to emulate the damage variance in the classic games
projectilemelee
This is the basic projectile attack.
It plays a throwing animation and fires halfway through. However if you're close it will switch to melee instead. However if you initiate it in melee range and move further it will switch to the projectile attack.
This emulates the behaviour of the Imp
projectilemeleemiss
This is the basic attack however it locks the AI into melee or projectile attacks.
If the player is in melee range it moves to the melee state and will do a full melee animation. This can be dodged.
If the player is too far away it chooses to do a projectile attack.
This emulates the behaviour of Revenant (though not really as the revenant would charge at the player from a certain distance)
projectileburst
This is a rapid fire barrage of 3 rockets. It shoots one rocket every .75 second, reloading and reaiming between shots.
If the player is in melee range it will melee attack them, this can be dodged.
It loops the firing animation, so choose one that looks good.
Change the shotsremaining 3 value to chance the volume of the barrage
Change the etimergreater=750 value to change the rate of fire
This emulates the behaviour of the Cyberdemon but adds melee
projectilespread
This fires 3 pairs of projectiles every .5 second in a spread pattern. First attack fires of (the AI's) right, the second straight forward and the final to their left.
If the player is in melee range it will melee attack them, this can be dodged.
It loops the firing animation, so choose one that looks good.
This one was challenging to do but works really well. It technically shoots 1 at a time but the time difference between the firing is so small it's not noticeable (aside from the phasing of 2 fire sounds).
Change the rotatey=(+ or -)5 value to get a bigger or smaller spread. Try to keep them to the same value or else it will mess with the spread
This emulates the behavious of the Mancubus but adds melee
semiautomelee
This is for single shot attacks, preferably with guns.
The AI will play it's attack animation before firing to give you a brief moment to get into cover.
Since the animation plays before firing this could be a reload or whatever.
This emulates the behaviour of pistol and shotgun zombies
fullautomelee
This is for continually firing attacks such as miniguns. The AI behaves the same as the semi auto attack however it will not stop shooting until the player is out of sight, dead or the gun is empty.
The firing animation loops during firing.
Change the etimergreater=125 to affect the rate of fire
This emulates the behaviour of the Chaingun zombie
chargemelee
This is for charging enemies. It will very quickly run to the player and if it hits them it causes damage. If it misses it keeps moving for a while. It uses setentityspeed=x to do this.
This script is very buggy and I could not find a solution for it. The enemy stops when reaching the target sometimes, sometimes it will not stop moving and will go straight through a wall despite constant raycasts to detect collision.
Also it resets the movement speed to 100 after a charge so don't bother changing the entity's movement speed. This is because it used movetotarget so that it works for flying monsters, this ignores floors (and other collision it seems)
Similarly my attempts with flying monsters were really unsuccessful so I never bothered to add real support for them.
I did want to make death scripts that would allow resurrection with an archvile style healer, however there is no way for that to work as entities cannot check a another's health or other variables.
However I could do this with activation checks, similar to how door opening works?
Similarly I could not think of a way to create a simple gibbing effect by spawning a ton of gore pieces. Sad.
The problems I've got so far...
Enemies can see you through walls it seems. I think that's an engine problem.
The tic system seems slow but I'm not sure why. I assume the script system updates every millisecond like the timers but it seems off.
I can't manage to make it detect if the AI is blocked by a dynamic entity as the raycast only hits static geometry. Using anywithin=x only results in it constantly detecting something (most likely itself) so that's useless.
Some walls, particularly those on the outside of a map will not be detected by raycasts and will make AI hopelessly walk into them for a while.
While these scripts might not be useful for everyone I hope that it provides a useful resource for the small community remaining here. Even as a memoriam to the engine, its abilities and limits.
If you have any problems please post them in the thread. And if you have any ideas for solutions or fixes to problems please post them.
If something compiles on the first try. Something is terribly wrong.