Room Escape Game in Unity3D – Part 1

UPDATE: ALL THESE AND MORE CAN BE FOUND ON MY NEW SITE http://www.returnofthebrain.co.uk/

In this tutorial we will go through the process of building a small game scene comprising a room that the player must ‘escape’ from by solving two puzzles in order to open a door. We’ll set up some objects that can be interacted with by clicking on them, then two different puzzle screens, a sliding blocks puzzle and a puzzle that involves dragging and dropping shapes to fit inside a square space.

The final playable result can be found here on itch.io, and the source code can be found here on github.

Project Setup

First thing we need to do, as usual, is set up a new Unity project, it doesn’t really matte if you use 2D or 3D mode for this as we’re going to be using the UnityGUI system by itself. Then you’ll need to import all the assets from the zip file above with a few key things to keep in mind:

  •  You’ll need to set every image to an import mode of Sprite if it doesn’t do so automatically.
  •  Except for the 3 images inside “UI/cursors” which will need to be set to a special import mode “Cursor”.

Next up you’ll need the standard set of folders inside your Assets folder: Prefabs, Scenes, Scripts and Sprites. Put all of the artwork into the Sprites folder.

Create a new scene and save it as “MainScene” or something else appropriate in the Scenes folder.

GUI and Scene Setup

Right click in the scene Heirarchy and select ‘UI/Canvas” to create our main GUI canvas, call this “GameCanvas”.

It comes with some pre-attached components, one of which, the Canvas Scaler, we need to adjust. You want to make sure it is set to UI Scale Mode – Scale With Screen Size with a reference resolution of 1920 by 1080 (the size of our backgrounds! If you’re using a different resolution as your target, use that.). You should also set the Match mode to “Match Width of Height” and the slider for it all the way over to Width, this should be the default.

02_blackbarsetup.gif

This allows us to place black borders at the top and bottom of the screen for monitors that are higher than 16:9. You can adjust the colour of these by selecting the main camera object and setting it’s Clear Flags property to “Solid Colour” and adjusting the Background colour property;

Then we will set up the elements needed for each of the 4 screens in our game: The main gameplay scene (our Grove), the victory screen and two screens that will house our minigames.

For each of these, we’ll need to right click the canvas object we created and add a “UI/Panel” object. Give each one an appropriate name, I’ve gone with:

  • “GroveScene”
  • “VictoryScene”
  • “Minigame_BlockPuzzle_Screen”
  • “Minigame_SlidePuzzle_Screen”.

You’ll want to make sure the ‘Image’ component that comes by default with each panel is disabled or deleted.

Lastly make sure each one is set to be anchored to the middle-center (click on the anchor preset button, then hold alt+shift and clicking on middle-center icon) and set to a width and height of 1920 by 1080 to match our target size (if you set it to stretch, it won’t maintain the proper aspect ratio when it scales!)

01_scenestructure

Setting up the Grove Scene:

  • Right click the GroveScene object and add a UI/Image, call it “background”.
  • Set its sprite to our ‘bg_grove’ image.
  • Ensure the colour property has no transparency, sometimes the default does
  • Right click the GroveScene again and add a UI/Panel object, call it “interactables”, this will house all of the objects in our scene that we can click on, again disable the Image component it comes with.
  • Set it’s anchor preset to be stretch in both directions, to match the size of its parent.

Add a background object to the VictoryScene object in the same way as above, but this time use the “bg_victory” object, then set the VictoryScreen object to be disabled, we won’t touch it again for a while. Also disable the two puzzle screen objects, we’ll come back to those in part 2.

Here’s a quick overview of how the scene and ui are setup inside of Unity.

04_scenelayout.gif

Interactable Class

Finally we can write some code! The first thing this project needs is the ability to be able to hover over an interactable object and to be able to click on it to interact with it. What that interaction is/does will vary, so we’ll need to group some common functionality into a base class.

Create a new CSharp class and call it “Interactable”.

You’ll need to include the UI and Eventsystems namespaces at the top.

using UnityEngine.UI;
using UnityEngine.EventSystems;

Then declare a variable, a Texture2D called “hoverCursor”, this will enable us to give each interactable a potentially unique cursor that will show when the user moves the pointer over it. Something a lot of classic adventure games do.

public Texture2D hoverCursor;

Next we need to tackle some important setup in the Awake method

public virtual void Awake () {

        EventTrigger eTrigger = gameObject.AddComponent<EventTrigger>();

        // pointer handling
        EventTrigger.Entry pointerClick = new EventTrigger.Entry();
        pointerClick.eventID = EventTriggerType.PointerClick;
        pointerClick.callback.AddListener(OnClick);
        eTrigger.triggers.Add(pointerClick);

        EventTrigger.Entry pointerEnter = new EventTrigger.Entry();

        pointerEnter.eventID = EventTriggerType.PointerEnter;

        pointerEnter.callback.AddListener(OnPointerEnter);

        eTrigger.triggers.Add(pointerEnter);

        EventTrigger.Entry pointerExit = new EventTrigger.Entry();

        pointerExit.eventID = EventTriggerType.PointerExit;

        pointerExit.callback.AddListener(OnPointerExit);
        eTrigger.triggers.Add(pointerExit);
}

The EventTrigger component is what allows things like UI Buttons to respond to UI events that unity generates. We’re going to use it here to allow us to know when the mouse pointer has moved into or out of the bounds of the object, and when the user clicks on it.

We can do this by creating an EventTrigger.Entry class containing the event we want to respond to and the callback we’d like to execute when we do. Then adding this entry to the list of triggers the EventTrigger we added will respond to.

You can see here we’ve subscribed to the PointerClick, PointerEnter and PointerExit events, now we need to write the methods that correspond to them.

public virtual void OnClick(BaseEventData data){
            Debug.Log("We clicked an interactable!");
    }

    public virtual void OnPointerEnter(BaseEventData data)

    {

        if (hoverCursor) {

            CursorMode cursorMode = CursorMode.Auto;

            Vector2 hotSpot = Vector2.zero;

            Cursor.SetCursor(hoverCursor, hotSpot, cursorMode);

        }

    }

    public virtual void OnPointerExit(BaseEventData data)

    {

        CursorMode cursorMode = CursorMode.Auto;

        Cursor.SetCursor(null, Vector2.zero, cursorMode);

    }

Each of these functions needs to take in a BaseEventData object, which contains various information about the ui event. We don’t need any of it right now, but seeing as the triggers expect an Action type we have to match that format. You can read more about Actions and Delegates in the C# DotNet documentation. The methods are also marked as ‘virtual’ because we’ll want subclasses to be able to define their own behavior with an override method.

OnClick simply prints out a debug message to confirm that it’s working.

OnPointerEnter is a bit more interesting, it looks to see if we have an image assigned to the public ‘hoverPointer’ variable we declared, and if there is one uses the Cursor.SetCursor method built into Unity to change the cursor graphic. We set the CursorMode to Auto and hotSpot to Vector2.zero. You can read more about what these do in the Unity documentation.

OnPointerExit sets the cursor back to the default one after we move the pointer back outside the bounds of the object.

note: you can read more about the unity events system here (https://docs.unity3d.com/Manual/EventSystem.html)

Lastly, we need a way of polling an interactable to see if it’s function has been ‘completed’, subclasses will override this method to see if their assigned puzzle has been completed, or maybe to see if the user has a particular item in their inventory, or something to that effect. For now we’ll just return true.

public virtual bool IsActivated()
{
    return true;
}

As we won’t be creating the puzzles until parts 2 and 3, we might as well set up some interactables to test with now. We can do this by adding more UI/Image objects to the ‘interactables’ ui panel we created earlier. Add 2 of them for now, and use the sprites that represent the two objects interactive objects, then position them over the two tree trunks, attach instances of this ‘Interactable’ script to each of them and set their cursor icon to something appropriate, I’ve chosen a question mark. Don’t forget to hit the “Set Native Size” button on the Image components, too, otherwise your images will be squashed.

03interactablesetup.gif

Note: These cursor icons needs to be imported specifically as cursor icons in the unity asset folder. Instead of being imported as sprites.

Interactable Door and showing the Victory Scene

The next thing we’ll do is build a door object that the player will be able to open if the two interactables in our scene have been ‘activated’ (this will eventually mean the user will have to complete both puzzles before they can open the door) and then use to go from this scene to the victory scene.

To create the door, go back to our GroveScene’s interactables object and create a new one, call it “interactable_door” and instead of attaching an Interactable script to it, create a new script and call it “Interactable_Door”. We’ll use this script to control the state of the door, and to activate the victory scene when the player opens it and steps through.

using UnityEngine.EventSystems;
using UnityEngine.UI;

public class Interactable_Door : Interactable
{

        public Interactable[] _requiredCompletedInteractables;

        public Texture2D _openHoverCursor;

        private Image _image;

        public Sprite _open;

        public Sprite _closed;

        public GameObject _currentScreen;

        public GameObject _destinationScreen;

        private bool isOpen = false;
}

void Awake()
{
      base.Awake();
      _image = gameObject.GetComponent<Image>();
      _image.sprite = _closed;
}

Make sure you inherit the class from Interactable by replacing “Monobehaviour” with “Interactable” in the class declaration.

It should be reasonably clear what each variable is for.

_requiredCompletedInteractables is an array of other interactable scripts that must be ‘activated’ in order for the door to be unlocked. In this case, we’ll drag the two other interatables in the scene into here, for the moment we won’t need to do anything with them to unlock the door, however.

We inherit the variables from our Interactable, so we already have hoverCursor, but we might want to show a different one when the door is open.

We need to have access to our Image component as we’ll want to change the sprite to show our door as either open or closed.

_currentScreen should reference the ‘GroveScene’ game object and the _destinationScreen is our ‘VictoryScene’ object. This will allow us to swap them over when the player goes through the door.

Finally ‘isOpen’ just allows us to keep track internally of whether or not the door is open.

In the Awake function, we need to call base.Awake() so that our event trigger code in the superclass (the class we inherited from, Interactable) runs for this object. Then we grab a reference to the image component and set the sprite to the closed door sprite.

    bool CanUse()
    {
        bool canUse = true;
        foreach (Interactable interactable in _requiredCompletedInteractables)
        {
            if (!interactable.IsActivated())
            {
                canUse = false;
            }
        }
     return canUse;
    }

We need to know if the requirements for being able to ‘use’ this object (open the door) have been met, in this case all of the scriptable objects in our array _requiredCompletedInteractables must be ‘activated’.

To find out, we start with a boolean value that is true, and use a foreach loop to iterate through every Interactable in the list, setting that value to false if we find any that are not active. Then we return that boolean value.

The reason this is a function and not a simple variable is that the state of the other objects can change, and we want to check when the user interacts with the door rather than have a messier system when the Interactables try to tell the door that they’ve been activated. This also makes it easier for it to be extended, for example if later on you decide you want to make this door unlocked based on a specific state of another object, such as the position of a lever or dial the player can interact with, but locked in all other cases, you can set that objects ‘activated’ state to correspond to that and the door would be able to re-lock itself if the player changes the state of the other object.


        public override void OnClick(BaseEventData data)

        {

            base.OnClick(data);

            if (!isOpen && CanUse())

            {

                // open the door!

                _image.sprite = _open;

                isOpen = true;

                // ensure the pointer is up to date

                OnPointerEnter(data);

            }

            else if (isOpen)

            {

                _currentScreen.SetActive(false);

                _destinationScreen.SetActive(true);

                // ensure the pointer is reset

                OnPointerExit(data);

            }

            else

            {

                // hmm, it's locked...

                Debug.Log("The door is locked...");

            }

        }

Now we need to override the OnClick function so that we can see if the door is open or closed, and proceed accordingly.

If we can use the door, and it’s not already open, we change the sprite so that the player can see the door is now open and set our internal ‘isOpen’ value to true so that we know the door is open from that point on. Otherwise, if the door is already open, we will disable the _currentScreen and enable the _destinationScreen. In a real game this would probably be a more generic scene change function. We also need to ensure that the cursor icon is reset, so we manually call the OnPointerExit function.

If neither of these conditions is met, we print out a debug log stating that the door is locked.

public override void OnPointerEnter(BaseEventData data)

        {

            if (_openHoverCursor && isOpen)

            {

                CursorMode cursorMode = CursorMode.Auto;

                Vector2 hotSpot = Vector2.zero;

                Cursor.SetCursor(_openHoverCursor, hotSpot, cursorMode);

            }

            else

            {

                base.OnPointerEnter(data);

            }

        }

        public override bool IsActivated()

        {

            return CanUse();

        }

The last thing we do in this script is override the ‘OnPointerEnter’ function to account for the fact we have a different cursor icon depending on the state of our door, and the ‘IsActivated’ function to only return true if the door is unlocked. Note that in OnPointerEnter we don’t call base.OnPointerEnter right away because we don’t need that functionality if the door is open and we want to display the secondary cursor.

Before we can play, you’ll need to assign the various public variables in the inspector. Drag the appropriate icons into the HoverCursor and OpenHoverCursor slots, assign the Open and Closed sprites for the door, and assign the GroveScene and VictoryScene as the CurrentScreen and DestinationScreen respectively.

05playing.gif

At this point, although it doesn’t look like much you should be able to ‘play’ the game, click on the closed door to open it, and then proceed through the door to the victory screen. Awesome! But it’s a little too easy don’t you think? In Part 2 we’ll add the first of two puzzles to spice things up a bit.

Advertisements
This entry was posted in Uncategorized. Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s