here. Of course for them, the concept was called “smart objects”. The idea was simple in its logic but had amazing implications.
In every beginner tutorial I had read through for game design. The item in question would be an enumeration. Then when the player would use it, the player’s code would have a dictionary of actions or a switch case and that would take you to the new actions logic. And every time you needed to add an item, you’d have to adjust the decision making code to take it into consideration.
Thus they introduced the concept of ‘smart objects’ which later seems to have just become the concept of object oriented code in general, but for some reason at least to me this way of thinking completely changed how I approached programming. In sim’s the player code was simple with regards to items. It would call a function, “Use”. And then the item would be in charge of doing everything including the animation. This means that when you wanted new behavior, new animations new anything all of your code would exist in this new item. It would be self contained and the player code would be safe from editing.
The benefits of this kind of coding become immediately apparent. And of course this way of thinking can be applied to lots of different code, not just ‘smart items’ or ‘action advertisements’. I’m going to cover how we accomplished our intractable system, while noting a few bugs we still have to work out with it.
So we are going to need an ActionAdvertisement class.
public class ActionAdvertisement
{
}
But since that’s not the driving force, we’ll leave it as is for now and fill it out as we go. So instead let’s start with what we call in our game the InteractionSystem. Specifically, we’ll start with the update loop because that drives everything.
public void Update()
{
if (!Player.ShouldReadInput)
{ return; }
FindFocusAction();
}
private void FindFocusAction()
{
ActionAdvertisement focusAdvertisement = PhysicsHelper.
GetClosest<ActionAdvertisement>(Player.Transform, targetPoints.InteractionPoint, IsAdvertisementAvailable);
if (focusAdvertisement == null)
{
RemoveFocusAdvertisement();
}
else
{
SetFocusAdvertisement(focusAdvertisement);
}
}
private bool IsAdvertisementAvailable(ActionAdvertisement advertisement)
{
bool isAvailable = !advertisement.AiOnly;
isAvailable = isAvailable && advertisement.ActionDistance != AiActionDistance.Internal;
isAvailable = isAvailable && advertisement.CurrentlyAdvertising;
}
So the logic here while a little difficult to read due to the long line,isn’t as complicated as it looks. I won’t get into the physics helper code right now, that will be it’s own post at a later point, but we are grabbing the closest Action Advertisement,that matches a pre-condition defined in ‘IsAdvertisementAvailable”. If ‘give me the closest object’ returns an object then we display it. If there is not one then we hide it.
That pre-condition checks a few things. We use the action advertisement for both players & ai, so we want to make sure that it’s not something marked specifically for the ai. We want to make sure it’s not something marked ‘internal’ which is basically some thing that doesn’t take physical space. (We’ll cover these ai decisions in it’s own post). And most importantly. We make sure the advertisement is currently advertising.
That is something we’ll add to the action advertisement to give the ability of altering it’s availability. For example: Only being allowed to eat fruit that is fully grown.
[SerializeField]
private bool _currentlyAdvertising;
public bool CurrentlyAdvertising
{
get => _currentlyAdvertising;
set => _currentlyAdvertising = value;
}
One important note, is that because we only ever look at objects within a certain range and then grab the closest one. The first benefit of this is that when you walk out of range of an item, then it no longer is marked as something you can focus on. The second benefit is that if there are two objects next to each other that we could interact with, you are only dealing with the one that is physically closer. There are more advanced solutions that include a priority amount on the action advertisement in question.
The benefits of having a priority amount, is that you can basically make your decision on which action to focus on by two different weights (distance & importance) which can sometimes help with obnoxious situations where for example the player is standing next to a shop keeper and there is an item they can use next to the shop keeper. Either the player just wants to talk to the shop keeper and the item keeps getting in the way, or they just want to use the item, and talking to the shop keeper gets in the way. Of course that brings the question of which is more important, the faster or more consistent. But that’s a design decision you can make if you add a priority weight. In our example, we are only using distance as a weight. And until it becomes a problem, we won’t be adding any unnecessary priority weights. We feel it’s best not to add complexity to solve a problem that might not even come up.
Now that we know what we want to focus on. We need to actually focus, or remove focus from the object in question.
private AdvertisementUiPanel advertisementUIManager;
private ActionAdvertisement focusAction;
private void SetFocusAdvertisement(ActionAdvertisement advertisement)
{
if (advertisementUIManager != null)
{
advertisementUIManager.DisplayAdvertisement(advertisement);
}
focusAction = advertisement;
focusAction.Observer = Player.Puppet;
}
private void RemoveFocusAdvertisement()
{
if (advertisementUIManager != null)
{
advertisementUIManager.HideInteractions();
}
if (focusAction != null)
{
focusAction.Observer = null;
}
focusAction = null;
}
So I’m not going to get into how to do a UI in 3D Space, but there will be a post about it. Our UI manager will take care of that. All we have to do is tell it what advertisement to display.
One thing to note, apart from simply setting the focus action, you’ll notice that we set something called the ‘observer’. This is basically used to give a soft lock to the action. If the player is trying to interact with something, it can be really obnoxious if the AI decide ‘Hey, I want to interact with that as well’. It also helps if all the AI don’t see an action object and go “Let’s all kick the same soccer ball”. Which was a legitimate problem with some our initial soccer ball code in our first draft of our AI system. We’ll put up a post later about our ‘spider soccer’.
So let’s add the observer to our action advertisement
[SerializeField]
protected bool debug = false;
[SerializeField]
private bool debugObserver = false;
private ControllablePuppet _observer;
public ControllablePuppet Observer
{
get
{
if (_observer == null || !_observer.isActiveAndEnabled)
{
_observer = null;
}
return _observer;
}
set
{
if (_observer == value)
{
return;
}
_observer = value;
if (debugObserver)
{
string observerName = value?.gameObject.name ?? string.Empty;
Debug.Log($"{gameObject.name} Observer:{observerName}");
}
}
}
We cover the null coalescing here.
We are also going to have a post covering properties more. But we use them here instead of a simple field to allow for easier debugging and because there is always a fear that the observer will be destroyed is disabled, permanently locking the action. We solve that by always double checking that the observer exists before returning the backing field.
Of course another question you might have looking at this, is what is a controllable puppet. We’ll of course cover that in it’s own post, but for now the simple explanation is that it represents an entity that can be controlled by either AI or Player.
From here we need to cover using the interact action. Luckily that’s just triggering the action on the item itself.
private void OnInteractClick(InputAction.CallbackContext obj)
{
if (Player.Puppet.IsTooBusyToMove())
{ return; }
if (focusAction == null)
{ return; }
focusAction.TakeAction(Player.Puppet);
}
We’ll cover the guard clauses in another post, but you’ll notice that when we call ‘take action’ we send in the player’s character. This was the only way we could find to allow the various items in the game to be able to drive change in the player without needing to constantly update the player for each item that needed to do something unique.
So the last thing to cover is what that take action really does. This is defined in the action advertisement class we defined earlier.
[Serializable] public class PuppetEvent : UnityEvent<ControllablePuppet> { }
[SerializeField]
protected PuppetEvent OnTakeAction = new PuppetEvent();
public virtual void TakeAction(ControllablePuppet user)
{
OnTakeAction?.Invoke(user);
}
We have a virtual function, so any class can easily override what we do with when the player click ‘interact’ but we also have a unity event. So that the behavior of ‘what happens’ doesn’t have to be defined in the action itself.
For example, we might have a tree script that is in charge of spawning apples and such. We can add an action advertisement to point to one function (say…shake tree) and then we could have another action advertisement pointed to another action (say…cut tree down).
And with that, we have everything we need to dynamically add items into the game without needing to alter the player or ai code base on a per item basis. I can’t say it fully embraces the ‘smart object’ system, but it’s definitely a step in that direction.