Room Escape Game in Unity3D – Part 2

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

Last time we constructed a set of objects we could respond to clicks on and a door object that we can open and proceed through to the next scene. This time, we’re going to create a base class that outlines some generic behavior for our minigames, and then extend it to make a puzzle that involves fitting blocks into a square space.

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

Designing Our First Puzzle – Blockfit

Before we jump into code we need to just define the behavior of our simple puzzle. We are going to provide the player with a handful of shapes which the player has to arrange into a square. Each piece will start outside of the square and will need to be dragged into the correct position by the player, if the piece is near to the correct location, it will snap into place. When the player has correctly placed all of the pieces the puzzle will mark itself as solved.

This means we will need to know the correct position for each piece beforehand, and then move them into their starting positions when the puzzle is instantiated. With that in mind, it’s logical that we will want to create an empty UI for our puzzle that will load the actual pieces from a prefab we create separately.

puzzle_design

On the left we have the solution to the puzzle, and on the right are the puzzle piece sprites. You’ll need to import these and set them up, if you’re using the ones included with the tutorial assets this is pretty simple. Import blocks.png into Unity as you would a regular sprite, then change the sprite mode to ‘multiple’ and click ‘Sprite Editor’ to see the sprite editor window.

sprite_blocks

You’ll need click and drag to create a new sprite from a section of the image, and then adjust the position of each side so that it fits the puzzle piece properly. You’ll want to name them appropriately too, I’ve gone with “block_T”, “block_Step”, “block_I” and “block_L” to match their shapes.

Setting up the UI

We need to set up a GUI screen for this puzzle to take place on. In part 1 we set up a GameCanvas object, as well as our screen panels. We now need to select the ‘Minigame_BlockFit_Screen’ panel we created and add a sprite to the image component. You’ll want to add an Image component and use the ‘bg_block_puzzle’ image included with the resources as the sprite. You’ll also want to make sure it is centered to hit ‘Set Native Size’ so that it’s correctly fitted.
gui_from_part_1.gif
Additionally we need to add empty panels that we can use in code to denote areas of the screen we need, in this case we need to know the square part of the screen that houses the actual puzzle, and the part that denotes where the pieces will be placed once the puzzle is started. Create two new ui panels, and remove the image components, then name them ‘puzzle_container’ and ‘puzzle_piece_container’ respectively. Then position them over the appropriate parts of the screen panel.
layout_overview
Once you’re done, it should look something like this…
Lastly, you’ll want to create a ui button, and place it somewhere appropriate on the screen so the player can use it to back out of the puzzle if they need to. You’ll need to set the button’s OnClick event to the screen panels ‘SetActive’ function with an input of false, this will disable the puzzle screen when the player clicks it.
 button_hidescreen_setup

Puzzle Prefab

In order to know the correct position for each piece of the puzzle and ensure we load the puzzle with the correct pieces, we need to set up a prefab that contains each piece in its correct position within the shape. Because we will be working with UI panels, you’ll need to ensure that the object is attached to the GameCanvas while we are setting it up, or a test canvas in another scene, or you won’t be able to see it!
To set up the prefab create a new ui panel on our GameCanvas, called ‘block_puzzle_one’ and position it in the same place, with the same dimensions as the puzzle_container object in our minigame screen panel. Now you need to create 4 sub-panels, one for each of the puzzle pieces, give them appropriate names and set their image components to use the correct puzzle pieces. Then position them correctly so they match the completed puzzle state.
puzzle_prefab_setup

Lastly, create a prefabs folder in the project heirarchy to store this, and drag the ‘block_puzzle_one’ prefab into it, then delete it from the GameCanvas panel as we will instantiate it ourselves at startup.

Minigame Class

Create a new script and call it “Minigame”

public class Minigame : MonoBehaviour {

    protected System.Action _onComplete;

    // ui canvas
    protected RectTransform _canvas;
    protected float _canvasWidth;
    protected float _canvasHeight;

    [SerializeField]
    protected bool _isComplete = false;

    public virtual bool IsComplete { get { return _isComplete; }}

    public void SetOnCompleteCallback(System.Action callback)
    {
        _onComplete = callback;
    }

    protected void SetComplete()
    {
        _isComplete = true;
        if (_onComplete != null)
        {
            _onComplete();
        }
    }
 }

This is the base class all our minigames will use, all it does is hold a reference to a RectTransform that will act as the screen for our minigame, and a bool that we can use to know whether or not the puzzle has been completed.

Coding the Blockfit Puzzle

Before we build the main puzzle class, we’re going to build some functionality that our individual pieces will need. Create a new C# script called ‘BlockFitPiece’.

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.UI;
    using UnityEngine.EventSystems;
    using System;

We need to include a few extra namespaces for our puzzle piece class that will give us access to UI events again.

[RequireComponent(typeof(RectTransform))]
    public class BlockFitPiece : MonoBehaviour
    {
        RectTransform _rt;
        bool isAnchoredToPointer = false;

        bool isPlacedCorrectly = false;
        public bool IsPlacedCorrectly { get { return isPlacedCorrectly; } }

        Action _onWasPlacedCorrectly;
        Vector3 _correctPosition;
        Vector3 _storedPosition;
        float _distanceTolerance;

Above our class decleration, we’ve use the RequireComponent attribute, this ensures that our puzzle piece must have a RectTransform attribute attached to it when we place the script onto the object. This will prevent us accidentally attaching it to something that’s it’s not designed for.
Next we declare the variables we’ll need to use:
‘_rt’ is a reference to the RectTransform component we will use
‘isAnchoredToPointer’ will be set to true if the pieces is currently being dragged by the mouse-pointer
‘isPlacedCorrectly’ keeps track of whether or not the piece has been placed in its correct position within the board.
 ‘_onWasPlacedCorrectly’ is a callback we will call when the piece is correctly placed to inform the main puzzle script.\
‘_correctPosition’ is the position the piece is supposed to be placed at to complete the puzzle.
‘_storedPosition’ is the current position the piece has been placed.
‘_distanceTolerance’ represents how close to the correct position we need to place the piece before it snaps to the correct position.
    void Awake()
    {
        _rt = gameObject.GetComponent<RectTransform>();

        // add the event triggers
        EventTrigger eTrigger = gameObject.AddComponent<EventTrigger>();

        // pointer down event
        EventTrigger.Entry downEvent = new EventTrigger.Entry();
        downEvent.eventID = EventTriggerType.PointerDown;
        downEvent.callback.AddListener(OnPointerDown);
        eTrigger.triggers.Add(downEvent);

        // point up event
        EventTrigger.Entry upEvent = new EventTrigger.Entry();
        upEvent.eventID = EventTriggerType.PointerUp;
        upEvent.callback.AddListener(OnPointerUp);
        eTrigger.triggers.Add(upEvent);
    }

The Awake function will seem familiar from Part 1, we again need to use the EventsSystem to respond to PointerDown and PointerUp events so that we can drag our puzzle pieces around the screen. We have assigned the corresponding events to the OnPointerDown and OnPointerUp functions that we’ll declare below.

    private void OnPointerDown(BaseEventData data)
    {
        isAnchoredToPointer = true;
    }

    private void Update()
    {
        if (isAnchoredToPointer)
        {
            // find the pointers current position and move there
            Vector2 pos;
            pos = Input.mousePosition;
            _rt.position = pos;
        }
    }

    private void OnPointerUp(BaseEventData data)
    {
        isAnchoredToPointer = false;
        if (IsWithinDistanceOfCorrectPosition())
        {
            // correctly placed
            isPlacedCorrectly = true;
            transform.localPosition = _correctPosition;

            // finally disable it
            enabled = false;

            if(_onWasPlacedCorrectly != null)
            {
                _onWasPlacedCorrectly();
            }
        }
    }

    public bool IsWithinDistanceOfCorrectPosition()
    {
        float distance = Vector3.Distance(_correctPosition, transform.localPosition);
        return distance &amp;lt;= _distanceTolerance;
    }

OnPointerDown is a very simple function, we just set ‘isAnchoredToPointer’ to true, everything else will be handled by the Update function which, if ‘isAnchoredToPointer’ is set to true, will position the piece at the mouse pointers screen location.
OnPointerUp sets ‘isAnchoredToPointer’ to false so that we stop dragging it, and then checks to see if we’ve placed the piece correctly by checking if we’re withing the distance tolerance of the correct position by calling IsWithinDistanceOfCorrectPosition. If it is, then we can set isPlacedCorrectly to true, and snap the transforms localPosition to the correct position to ensure it’s in the right place.
Then we disable the piece script as we won’t need it after that, and we don’t want the player to be able to drag pieces around once they’ve been correctly placed. Finally, if we have a callback assigned, we call it to inform whatever it controlling the pieces that it’s been placed correctly.
    public void SetCorrectPosition()
    {
        _correctPosition = transform.localPosition;
    }

    public void SetStoredPosition()
    {
        _storedPosition = transform.localPosition;
    }

    public void SetOnPlacedCallback(Action onPlaced)
    {
        _onWasPlacedCorrectly = onPlaced;
    }

    public void SetDistanceTolerance(float tolerance)
    {
        _distanceTolerance = tolerance;
    }
Lastly we’ve declared some public functions that the main puzzle script will use to inform the pieces what their correct and starting positions are, what the _onWasPlacedCorrectly callback should be, and what distance tolerance to use.

Blockfit Puzzle Script

The BlockFitPuzzle script itself will keep track of the pieces and the state of the puzzle, and handle setting up the puzzle pieces, checking whether or not it’s been completed and then responding appropriately.

    RectTransform _pieceStoreCanvas; // for the unplaced pieces to start/return to
    List<BlockFitPiece> _pieces = new List<BlockFitPiece>();
    float _placeDistanceTolerance = 30.0f;

We don’t need that many variables here, we need the RectTransform that will contain the pieces at the beginning of the puzzle, which I’ve called the ‘piece store’, we need a list of BlockFitPieces which will be our puzzle pieces, and a distance tolerance setting for them, this will control how close to their initial positions they need to be placed before being considered ‘correct’.

public static BlockFitPuzzle CreatePuzzleWithParent(RectTransform boardParent, RectTransform pieceStoreParent, GameObject puzzlePrefab){

    // create the piece store canvas
    GameObject pieceCanvasObject = new GameObject("PieceStore");
    RectTransform pieceCanvas = pieceCanvasObject.AddComponent<RectTransform>();
    pieceCanvas.SetParent(pieceStoreParent);
    pieceCanvas.localPosition = Vector3.zero;
    pieceCanvas.localScale = Vector3.one;
    pieceCanvas.sizeDelta = pieceStoreParent.sizeDelta;

    // create the puzzle canvas and pieces
    GameObject puzzleCanvas = Instantiate(puzzlePrefab);
    RectTransform canvas = puzzleCanvas.GetComponent&amp;lt;RectTransform&amp;gt;();
    canvas.SetParent(boardParent);
    canvas.localPosition = Vector3.zero;
    canvas.localScale = Vector3.one;
    canvas.sizeDelta = boardParent.sizeDelta;

    BlockFitPuzzle blockFitPuzzle = puzzleCanvas.AddComponent<BlockFitPuzzle>();
    blockFitPuzzle.CreatePuzzleBoard(canvas, pieceCanvas);

    return blockFitPuzzle;
  }

This is the only function here marked as static, and there’s a good reason for this. We want the puzzle to be in control of setting itself up, so this function allows us to tell it which panels it should use and which puzzle prefab to use, but then lets the puzzle take care of starting itself up from there, before returning a reference to itself so we can track whether or not it’s been completed elsewhere.
The function itself is quite simple, we first create a duplicate of the piece store panel, and parent it to the original, this allows us to clean it up a bit easier later on. Then we instantiate the puzzle prefab and parent it to the boardParent panel, which will be our ‘puzzle_container’ panel in the GameCanvas.
Then we attach an instance of the BlockFitPuzzle component to the puzzleCanvas object and call CreatePuzzleBoard before returning it.
public void CreatePuzzleBoard(RectTransform boardCanvas, RectTransform pieceStoreCanvas){

        _canvas = boardCanvas;
        _pieceStoreCanvas = pieceStoreCanvas;

        // get all children on the board canvas, these are our actual pieces
        foreach(Transform t in _canvas)
        {
            BlockFitPiece piece = t.gameObject.AddComponent<BlockFitPiece>();
            piece.SetOnPlacedCallback(OnPiecePlaced);
            piece.SetDistanceTolerance(_placeDistanceTolerance);
            _pieces.Add(piece);
        }

        StartCoroutine(SetPieceStartingPositions());
  }

CreatePuzzleBoard stores a reference to both the puzzle board and the piece container panel we will pass in, and then iterates through each of the child objects on the puzzle board, which should be our 4 shapes, and adds a BlockFitPiece component to them before storing a reference to it in the pieces list. We also set the OnPlacedCallback of each piece and the distance tolerance.
public IEnumerator SetPieceStartingPositions()
{
    yield return 0;
    // place them randomly within the piece container rect

    float inset = 40f;
    float max_x = (_pieceStoreCanvas.sizeDelta.x / 2) - inset;
    float max_y = (_pieceStoreCanvas.sizeDelta.y / 2) - inset;

    foreach (BlockFitPiece piece in _pieces)
            {
        // set the correct position
        piece.SetCorrectPosition();
        // parent it to the piece store container to work out the position a bit easier
        piece.transform.SetParent(_pieceStoreCanvas);
        piece.transform.localPosition = new Vector3()
        {
             x = Random.Range(-max_x, max_x),
             y = Random.Range(max_y, -max_y),
             z = 0.0f
        };

        // parent it to the puzzle canvas again so it'll set the correct position
        piece.transform.SetParent(_canvas);
        // set the stored position
        piece.SetStoredPosition();
    }
}

The reason we need to use an IEnumerator for this is because quite often in Unity, if you try to access size/position variables for UI elements on the same frame they are instantiated, you’ll get incorrect results back as the engine hasn’t finished setting them up yet. So to ensure we get the proper values back, we need to wait a frame before reading them.
What this function actually does is take each piece, and place it randomly within the puzzle_piece_container area.
public void OnPiecePlaced()
{
    // check if it's within the target distance
    // if it is, mark it as being completed
    CheckComplete();
}

OnPiecePlaced will be set as the callback our BlockFitPiece scripts call whenever one of them is placed correctly, as we don’t really need to do much else we just call CheckComplete from here.
void CheckComplete()
{
    bool correct = true;
    foreach(BlockFitPiece piece in _pieces)
    {
        if (!piece.IsPlacedCorrectly)
        {
            correct = false;
        }
    }

    if (correct)
    {
        SetInputEnabled(false);
        SetComplete();
    }
 }

CheckComplete is another relatively simple piece of code, we check each piece to see if it’s been placed correctly, and if any of them haven’t, we return false. If they have all been placed correctly, we disable input and set the puzzle as complete by calling SetComplete.
void SetInputEnabled(bool isEnabled)
 {
    foreach(BlockFitPiece piece in _pieces)
    {
        piece.enabled = false;
    }
 }

The quickest way to disable input for this puzzle is simply to disable the BlockFitPiece scripts on each of the pieces, as they are the only things that actually respond to input.

Setting up the Puzzle

Now that we have defined the ui elements that we need to make the puzzle function, we need to replace our default Interactable script with one that will bring up the puzzle UI when the user clicks on the object.

    using UnityEngine.EventSystems;

So create a new script called “Interactable_BlockFitPuzzle” and include the EventsSystems namespace as we need access to that.

    public class Interactable_StartBlockFitPuzzle : Interactable

You’ll also need to inherit the class from Interactable rather than Monobehaviour.

Next we need to be able to assign the parts of the puzzle UI that the puzzle needs to know about in the inspector, so declare them as public variables.

    public RectTransform _blockPuzzleScreen;
    public RectTransform _blockPuzzleContainer;
    public RectTransform _blockFitPieceStoreContainer;
    public GameObject _blockPuzzlePrefab;

    private BlockFitPuzzle _blockFitPuzzle = null;
We’ve declared a RectTransform variable to represent the puzzle screen, the puzzle container and the piece store. These serve 3 purposes as outlined in the puzzle script we wrote earlier.
  • Screen: This is the parent object for the UI that contains all of our puzzle.
  • PuzzleContainer: This is the space that is occupied by the actual ‘board’ of our puzzle, where the player needs to put the pieced.
  • Piece Store: This is a convenience thing for us, a place to put the pieces outside of the puzzle for the player to use.
The last public variable is a GameObject prefab that represents the finished puzzle, from this we’ll get the correct positions of all the pieces contained in the prefab, and then the puzzle will handle moving them to the piece store so that the user can start the puzzle.
Lastly we declare a private instance of BlockFitPuzzle that we will create on starting the puzzle.
public override void OnClick(BaseEventData data)
{
    base.OnClick(data);

    _blockPuzzleScreen.gameObject.SetActive(true);

    if (_blockFitPuzzle == null)
    {
        _blockFitPuzzle = BlockFitPuzzle.CreatePuzzleWithParent(
                _blockPuzzleContainer,
                _blockFitPieceStoreContainer,
                _blockPuzzlePrefab);

        _blockFitPuzzle.SetOnCompleteCallback(OnCompletePuzzle);
    }
}

We want to override the base Interactable’s OnClick behaviour here. We run the base.OnClick function before setting the puzzle screen to active. Then, if we don’t already have a BlockFitPuzzle in progress, usually in the first instance the user clicks the Interactable, we create one by passing in the puzzle container, the piece store and the prefab that contains the puzzle pieces. Then we set the completion callback so the puzzle can inform us when it’s complete.
void OnCompletePuzzle()
{
    OnExitPuzzle();
}

void OnExitPuzzle()
{
    _blockPuzzleScreen.gameObject.SetActive(false);
}

Our OnCompletePuzzle callback is very simple, all it does is close the puzzle screen using OnExitPuzzle. The reason OnExitPuzzle is separate is because we may also want to exit the puzzle in an incomplete state if the user decided to quit.
public override bool IsActivated()
{
     return _blockFitPuzzle != null && _blockFitPuzzle.IsComplete;
} 

Lastly we override the IsActivated function to only return true if the puzzle has been completed. This means the door will now no longer open, until the puzzle has been completed.
settingup_interactable puzzle.gif
Now we can replace the script on one of our interactable objects with this one, and assign the public variables from the BlockFitPuzzle screen.
success.gif
If you hit ‘play’ now, you should not be able to open the door without first solving the blockfit puzzle! Next time we’ll be setting up a more complex sliding-tiles puzzle.
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