Mechanics: Artificial Agents – Addendum

A while ago, I wrote about artificial agents and using a simple AI to steer overarching character behaviours. Unfortunately, when further playing with the system, I found myself dissatisfied with a specific part of it. So now, I’d like to write an addendum explaining why I think it doesn’t work in the context where I tried to use it and how I’m fixing it.

Here’s the original article: Mechanics: Artificial Agents.

So, first off, the system still works well at a coarse enough scale. It was only when I took it down to a lower level that it started displaying behaviours I wasn’t too keen on.

The system works as intended at a coarse scale, where you pull away from the actions enough in terms of both time and space. Characters carry out their plans based on the motivations driving them and all that. It all works as long as the actions they take are general enough. But I wanted to take it lower down and use it to get the characters organising their time based on it, and I found two specific issues that break it.

Those two issues are a character’s circadian rhythm and locational convenience. Let’s explain…

Characters, generally being people in this context, usually have a specific circadian rhythm, the way they wake up and go to sleep, as well as organising their time (i.e. dividing it between work and rest). Now, the specific rhythm a character can follow can be unique. So, for example, while humans are majorly diurnal, that isn’t always the case in practice – like when someone works the night shift, and there also are cases where a person disrupts their sleep cycle for various reasons. So I will aim at an implementation that can handle all those issues.

Locational convenience stems from having the characters move around the world map. With the current implementation, there’s an issue where characters can decide to go somewhere, arrive there, and instantly realise they want to go elsewhere. This is because they don’t think about their current location being convenient for pursuing their goals. This leads us to factor in the location regarding a character’s desire to act. And at face value, it would be possible to include it as a motive. But unfortunately, that leads to an issue. Being at a location is a binary factor. Thus it either wholly does or wholly does not count towards the calculation. And if it’s a factor that should have a bearing on a character’s actions, it has to be high enough valued where just being at a location becomes a factor that can make a character act. So the solution shouldn’t cause this to happen.

Alright, those are the two issues. Now let’s get to how this changes the algorithm. The change is small; the only thing we want to alter is this function (and resolve some consequences of extending it):

Motive(a):

return ∑((dₘ, dₚ) ∈ dₐ) dₘdₚ

And we want to include these factors we’ve discussed as a separate part of the d component of our structure, which at the starting point is this:

d = {(dₘ, dₚ)}

This means that d is a set of pairs representing the values of motives and their weights. We’ll extend it to this:

d = (f, d’) = ({fₙ}, {(dₘ, dₚ)})

Meaning that d is now a pair consisting of two sets. The first set are the factors for what I’ve described above, and the second set is the one we had previously. All factors belong to a set of factors F. We’ll also need to update this set, so… we’ll need to change our update step from this:

D := {d ∈ D: d + Δd(Ω))

To this:

D := {d ∈ D: d + Δd(Ω))

F := {f ∈ F: Δf(Ω))

You might note that this means that factors are not changing over time, but they are set by the situation within the world, like where a character is or what objective (not delta) time it might be. So, finally, with all of this out of the way, we can redefine our function as:

Motive(a):

return (Π(f ∈ fₐ) f) ∑((dₘ, dₚ) ∈ d’ₐ) dₘdₚ

Right, but there’s one more thing I wanted to cover. I want to talk a little about the Δf(Ω) function in cases where f has to do with objective time. Because while it’s obvious what that function is in the case of locations, we can express it simply as an Iverson bracket (and using some EAV/OAV syntax for the location, and intensity of iₗ):

Δfₗ₌ₓ(Ω) = iₗ[location(pc) = x]

But that doesn’t work for time. So instead, I suggest using a relatively simple formula:

Δfₜ(Ω) = iₜclamp(0, dₜ⁻¹(-|(tₜ – aₜ)| + 0.5sₜ), 1)

So what the heck is this? It’s a function that is greater than 0 over a span of sₜ, with the middle of the span at aₜ, a max value of iₜ and a ramp to and from that value of length dₜ. Hard to follow? Look at the picture:

f

The picture. Have you looked at it?

Now, that shape might be familiar to you, this is something you see a lot in the case of fuzzy sets and their membership function, and while this isn’t the same thing, it’s not accidental that it looks similar. It follows the same logic but in a more simplified way.

It means that when talking about time, we can select spans where the character is interested in acting in a particular way. But because ramps exist on both sides, we ease in and out the effect this has on the character. Furthermore, because both this function and the previous one are used to multiply the value of the other desires, neither of them can cause a character to act in a way that is not supported by other motives. Instead, they merely weigh the previous value (i.e. the one I used in the original article).

Anyway, this gives me better results, but I still want to think about how to implement it well in the game engine.

Leave a comment