i've been working on enemies so that the player will have more than environmental obstacles to deal with. i'm starting with a typical gunner enemy: they patrol routes and shoot the player on detection.
walk cycles
i started with movement animations. the model and all of the animations are from mixamo for now but that'll change overtime. i want the movement to be pretty granular because i plan for the enemies to be a bit smarter than the usual video game enemy with a gun. they should have dynamic dispositions (calm, alert, alarmed, panicked) which affect their behavior. they might aggressively move toward you while firing, or maybe they stay put and wait for you to come out. if they're scared, they might try to run away. i'll cover more of this in a sec
aiming and inverse kinematics
although i have an animation for the enemy aiming down sights, it doesn't actually move and aim their body toward the actual place they're aiming. for that it needs inverse kinematics. thankfully godot 4.6 re-added their inverse kinematic nodes and they're apparently better than before. it wasn't super intuitive at first but i got it eventually! the new nodes work as a series of modifiers to the skeleton. so first i made a node that represents the point that the enemy should look/aim at, then i added the following nodes to the skeleton
-
spine toward target (LookAtModifier3D)
- this required a bit of configuration and had to come first because it's a parent of every other bone in the skeleton
- it needs an offset because the chest shouldn't point directly at the camera
- it also needs angle limits to prevent it from rotating to weird angles. it doesn't need to move a lot, just enough to sell the rest of the aiming
-
head toward target (LookAtModifier3D)
- this one is simpler because the head should look directly at the target
- it does have angle limits to prevent the head from twisting like an owl's head though
-
right hand toward target (LookAtModifier3D)
- this one was the hardest to figure out. the gun needed to point at the target, but the hands needed to stay fixed on the gun. trying to mix most combinations of BoneAttachment3D and IK would lead to a circular dependency when godot tried to resolve the position (which was really really funny)
- the right hand is responsible for pointing at the target. only the reloading animation involves removing the right hand from the gun so it seemed like the right choice
-
left hand on foregrip (TwoBoneIK3D)
- this attaches the left hand to the front underside of the rifle which keeps everything in sync
- its a bit scuffed right now but it mostly looks ok. it's a simple TwoBoneIK3D with the target node being a Marker3D that's a child of the gun object
-
rifle attachment (BoneAttachment3D)
- this attaches the gun to the right hand. since the right hand isn't attached to the gun, this avoids the circular dependency
since the right hand, head, and spine all face the same target, this means aiming is as simple as moving that node around. if i want the enemy to look around without aiming, i can turn off the right hand IK. right now, the enemy can't look in one direction but aim in another, but i dont think thats gonna be a big deal. i can always add that later anyway by separating the aim target from the look target.
ai
i don't have any good videos of this right now since im still working on it but i'll briefly cover it. the high level ai is handled by a hierarchical task network which sounds a lot more impressive than it really is. an HTN is a a planner that examines all possible tasks and determines which plan is achievable. as suggested by the name, tasks are hierarchical and can contain additional tasks -- usually referred to as compound and primitive tasks. each task may have a set of preconditions and effects. a precondition is the requirement for the task and the effect describes the impact it will have once completed.
task: eat precondition: is hungry = true effect: is hungry = false
to handle tracking preconditions and effects, HTNs will usually have a context object which acts like a blackboard. with each successive task, the plan will record the effects on a copy of the context object and pass that to the next task. if every a task fails, it backs out and drops all of the context changes to move onto the next possible plan.
what this looks like in the game is something like this
-
take cover
precondition: is alarmed = true, is covered = false
- find nearest cover
-
move to next waypoint
effect: is covered = true
-
reload
precondition: is magazine empty = true
effect: is magazine empty = false
-
patrol
precondition: is calm = true
- idle
- find next waypoint
- move to next waypoint