Hello everyone once again! Today I’m writing a post about Unity! I started developing using Unity just a few months ago, but despite having some experience in development, this is my first time with videogames.
I believe that explaining each step while having zero experience myself might be interesting and could provide other developers with some insight of the challenges that come with developing games.
I’m a newbie when it comes to developing videogames in general, and more particularly using Unity, so feel free to make any suggestions you might have, and also let me know if you believe there are more standardized ways to solve the problems I’m about to propose.
As a final note before we dive in, I would like to remind you that I’m no artist at all, therefore I won’t be creating any assets for the game from scratch (like 3D models, textures, animations and so on). I’ll be using free asset packs from the unity store :)
Step 0 - Preparing the basic environment.
Everything being developed in this post is publicly available in this repository.
Every step is going to be also accessible via Tags. For example, you can check the final code of step 0 here.
In this first part we are only setting up the minimum things we need to start working. This includes a first person player controller that was also implemented by me (not covered in this post). A ground plane with a wood tiling downloaded from the Assets Store (this package) and a door also downloaded from the assets store (this package).
That door is the one we are going to animate later for opening and closing it.
For the character movement I've set up the new input system so we can move with the standard WASD keys.
Feel free to check out the project and explore everything in there until you feel comfortable to move to the next step.
Note: For showing the cursor while playing the game, press ESC
Step 1 - Animating the door
In this step we are going to animate the door so it can be opened and closed. For that we are going to make the door rotate around the hinges, by rotating the SimpleDoor:Global_ControlSimpleDoor_MainDoor_LOD1 component around the Y axis.
First things first: If you haven’t done it already, make sure to display the animation window by clicking Window > Animation > Animation. Then switch to that tab, click the door and, in the Animation tab, click to create a new animation. Call it "Door".
Once we have the animation file, we are going to create the animation itself. Switch to the Animation tab and click the SimpleDoor:Global_ControlSimpleDoor_MainDoor_LOD1 component inside Aparment_Door prefab.
Now click the record button in the Animation tab and right click the Rotation attribute inside Transform section of SimpleDoor:Global_ControlSimpleDoor_MainDoor_LOD1 and click Add Key.
That will create a key frame in the frame 0 with the rotation set to (0,0,0). Then move to the frame 120 and change the rotation around the Y axis to 97º. It's important to change it in the rotation section instead of rotating directly in the scene view.
Now you should have 2 key frames, so you can stop recording and save the animation.
Now that we have the animation ready we have to set the animation controller up so we can control when to play the animation and how.
Show the animator tab by clicking Window > Animation > Animator and click the Aparment_Door prefab so you can manipulate the animation state machine of the door.
We are going to create a new state and call it Idle. Then make the layer default one and uncheck the "Write Defaults" checkbox.
Next step is create a transition from the Idle state to the already existing Door state and uncheck the "Has Exit Time" checkbox from the transition. Also uncheck the "Write Defaults" checkbox from the Door state.
Finally, the last steps we need to follow are to change the door state name to DoorOpen, and create a new Float parameter which we will call direction. That param will be used to control whether the animation has to be played regularly or backwards. To make that happen we must go to the DoorOpen state and under Speed parameter there will be a greyed Multiplier parameter. That is what is going to allow us to reverse the animation, so check the "parameter" checkbox and the drop down should show the "direction" parameter we created earlier.
To check that everything is working fine we have to play the game and check that the Idle state is played on loop.
Access all the step changes here.
Step 2 - Making generic actionable objects
Now it's time to code!
We are going to start creating a generic type for defining actionable objects. Since we are going to implement not only standard actionable objects but also actionable by key or by trigger (in upcoming posts), we'll create the foundations first.
We are going to use inheritance for that. If you don't know what inheritance is, is a technique that allows us to reuse and extend functionality by using parent-child classes. For a more extended a technical definition, head to someone who knows more than me.
We will see each part in detail when we reach them. We are going to define an interface to make our life easier when it comes to detect an actionable object and also a specific class for each type of actionable object, extending the generic classes with specific functionality. And for the key objects specifically, we will take advantage of the C# events.
Start creating the following folder structure:
├───Scripts │ ├───ActionableObjects │ │ └───Generics
All of the files created in this section are going to be placed inside the
Generics folder.
Defining the interface
Our interface is an easy one:
public interface IActionableObject { void Interact(); string GetInteractionText(); bool IsInteracterActive(); }
We have to define an
Interact method which will be called when the actionable object is interacted and will trigger whatever behavior we want.
GetInteractionText returns the text to show when looking at the object (if any). For example: "Press A to open".
IsInteracterActive is for telling us if the object is interactive or not (for a key object, it will return false until the key is picked up).
ActionableObject and InteractionText
Now that our interface is ready to be used to detect actionable objects, let's move to our other two generic components. First the InteractionText:
[RequireComponent(typeof(Text))] public class InteractionText : MonoBehaviour { private Text m_text; private string m_initialText; private string m_interactionText; private void Start() { m_text = GetComponent<Text>(); m_initialText = m_text.text; } private void Update() { if(m_text.enabled) { m_text.text = $"{m_initialText}{m_interactionText}"; } } public void SetInteractionText(string interactionText) { m_interactionText = interactionText; } public void Enable() { m_text.enabled = true; } public void Disable() { m_text.enabled = false; } public bool IsEnabled() => m_text.enabled; }
The first thing we are going to do in this code is to forcefully bound the script to a GameObject with a Text component.
The class will have a referente to the
Text component and two attributes for holding the initial text and the interaction text.
We are going to use the
Start method to store the reference to the text component and setting the
m_initialText to the text inside the component.
In the
Update we are just setting the text of the component to a combination of the initial text and the interaction text. With that, we can add a common starting text to all interaction texts if needed. For example prepending "Press X to" to every interaction text.
The last thing is a function for changing the interaction text.
NOTE: We could enhance the performance by having a flag indicating when the interaction text has changed and only updating it when changed, instead of in every
Updatecall.
And now our ActionableObject:
public abstract class ActionableObject : MonoBehaviour, IActionableObject { [Header("UI")] [Tooltip("The text that will appear in the overlay before interacting with this object")] [SerializeField] string interactionText = "open"; [Tooltip("The text that will appear in the overlay after interacting with this object")] [SerializeField] string interactionTextReverse = "close"; protected bool m_interacted = false; protected bool m_interactive= true; private void Start() { InnerStart(); } protected abstract void InnerStart(); public string GetInteractionText() { return !m_interacted ? interactionText : interactionTextReverse; } public void Interact() { InnerInteract(); m_interacted = !m_interacted; } protected virtual void InnerInteract() {} public virtual bool IsInteracterActive() => m_interactive; public virtual void SetIsInteractive(bool isInteractive) => m_interactive = isInteractive; }
This class must be abstract because we need to force a specific implementation for each type of actionable object.
First, we define some serialized fields that we will need. The
interactionText is "open" by default (remember we are prepending it with the
m_initialText of the
InteractionText). The
interactionTextReverse is the text to show to "reverse" the action (if needed). For example open/close.
The attributes
m_interacted and
m_interactive indicates whether the object is in the "interacted" state (interacted and not reverted) and if is interactive, respectively.
The
Start method does nothing else than calling an abstract
InnerStart. This is done in order to avoid a reimplementation of the generic
Start without calling the super. This is useful when we need to force some behaviour in the parent class that cannot be overriden (as in the
Interact function). Is some sort of template method.
GetInteractionText just returns the interaction text or the reverse one depending on the
m_interacted flag.
Interact is similar to the
Start function. It's called when the object is interacted and just calls the
InnerInteract (which will hold the specific interaction code) and flips the
m_interacted flag.
IsInteracterActive and
SetIsInteractive are the getter and setter for the
m_interactive flag.
And that's all for this step! We have just implemented our generic actionable objects! Let's move to the next step!
Remember, you have the code developed in this step here
Step 3: Standard actionable objects
Here we are going to implement the specific class for a standard actionable object. This means that it's not actionable by key, that triggers an animation and that is "reversible" (you can, for example, open and close a door). I consider this the standard since it was the first type of actionable object I implemented and will be the one which will be used more extensively in our game.
You may remember that we used some hardcoded strings in the editor when setting up the animations (a parameter called "direction"). Instead of using literals we are going to extract that to constants, to avoid having hardcoded strings across all our codebase.
Create a folder called "Constants" inside our "Scripts" folder and, inside it, create a file called "Animation":
namespace AnimateDoors.Animation { public static class AnimationConstants { public static string ANIMATION_DIRECTION = "direction"; } }
We are isolating those constants in the
Animation namespace to avoid collisions with the script name and the Unity Animation component. We are defining a constant called
ANIMATION_DIRECTION with the direction parameter name.
Now let's create our
StandardActionableObject class inside our
Scripts/ActionableObjects folder:
[RequireComponent(typeof(Animator))] public class StandardActionableObject : ActionableObject { [Header("Animation")] [Tooltip("The animation state to play when interacting with this object")] [SerializeField] string animationState; protected Animator m_animator; protected override void InnerStart() { m_animator = GetComponent<Animator>(); } protected override void InnerInteract() { UpdateAnimation(); } private void UpdateAnimation() { m_animator.SetFloat(AnimationConstants.ANIMATION_DIRECTION, !m_interacted ? 1f : -1f); m_animator.Play(animationState); } }
First we are going to require the Animator component to trigger the animation.
Then, define an attribute to store the name of the state in the animator to play when the object is interacted.
The
InnerStart will just store a reference to the
Animator.
The
InnerInteract will call to
UpdateAnimation and, that one, will set the "direction" parameter to 1 or -1 depending on the
m_interacted flag. Remember that the
direction controls the speed of the animation. If the object has not been interacted we are going to set the speed value to 1 and the animation will be played forward. If it has been interacted, the direction will be -1, so it will be played backwards. Right after that, we will play the state stored in the
animationState attribute.
And that's all! Easy, right? That's why we invested some time in the parent class, so the children are easy to implement.
Access the code of this step here.
Let me stop here for now and finish the implementation in a following post. In the next one we will implement the specific classes for each type of actionable object and set everything up in the editor to use it.
Please feel free to make any suggestions and propose improvements to the code in this post, as well as any doubt you might have! Stay tuned for the next post in which we will finish implementing our key objects!
Discussion (0)