In the final part of this tutorial series, we'll finish building our Fez-like game. Now that we have the basic level and character out of the way, we'll need to add code to handle the 3D rotation.
In order to do our 3D rotation, we will need to build a 3D level. Rotations in the level will occur at 90 degree intervals. Depending on which direction we're facing, we will show the
front, right, back or left side of the level.
In 3D, we have a width, height and depth to consider.
However when playing the game, the player will experience the location of all 3D platforms in 2D, essentially the depth is ignored and only the X and Y coordinates on the
screen are occupied by platforms. Depending on the orientation of the platforms and the
direction the player is facing, it's possible that the 3-dimensional location of the player and platform
will not align, so even though it appears the player could jump onto a platform, they will end up missing it. To prevent this, we will create invisible cubes (invisicubes) for the player to land on.
This will make it look like they have landed on their desired platform.
When playing, invisicubes will be created at the appropriate locations to give the illusion of standing on a platform when viewed with a 2D orthographic camera
Basic Rules
We will setup a script to handle the following:
When we aren't jumping, check if we're on an invisible platform, if so move to the closest
physical platform. Try to move to the closest platform to the Camera, this will
be the platform the player expects to be on. If we made any changes to the player depth, update the
location of our invisible cubes.
Note: If we allowed the player to rotate on invisible platforms, it becomes disorienting and places them at locations that are unintuitive. This is why we need to move the player to the correct physical platform.
Note: If we allowed the player to rotate on invisible platforms, it becomes disorienting and places them at locations that are unintuitive. This is why we need to move the player to the correct physical platform.
Rotation
In Fez, the core gameplay mechanic is solving a 3D puzzle by
rotating in the game world. At any one
time we will only view a single side of the world using an
orthographic camera. This removes
perspective, meaning it will appear that platforms which are
deeper or closer to the camera will
all be viewed at the same depth. We only get the sense of
the 3D world when rotating, otherwise
it all looks 2D.
Let's start by making an invisicube which we will be using extensively. To do this, create a new Cube in your scene, name it Invisicube. All we need to do now is disable the MeshRenderer component and drag the Invisicube into your Assets folder to create a Prefab.
Note that the position is not important, but you will need an enabled Box Collider. The Mesh Renderer should be disabled or removed
We will create a new script to manage some of these aspects. This script is a bit long, but I will describe the gist of it. Create a new C# script in your project and name it FezManager. Copy in the following code:
using UnityEngine; using System.Collections; using System.Collections.Generic; ////// Fez manager creates invisible cubes for the player to move on. The world is based in 3D to allow rotation, this means /// there are varying levels of depth that the player could be at, these may not line up with the physical platforms we create. /// The player is moving in 2D, so it looks like a platform is present where one may not be, depending on the depth of the platform /// and the player. If they are different, we will create invisible cubes that the player can move on to fake the player being on /// a 2D platform. When we have the chance, we will move the player's depth to the closest platform so when they next rotate /// it will not disorient them. /// public class FezManager : MonoBehaviour { //Script that controls the player sprite movement and animation control private FezMove fezMove; //Keeps track of the direction our player is oriented public FacingDirection facingDirection; //Access to the player gameObject, useful for getting spacial coordinates public GameObject Player; //Used to tell the FezMove script how much to rotate 90 or -90 degrees depending on input private float degree = 0; //Access to the Transform containing our Level Data - Platforms we can walk on public Transform Level; //Access to the Transform containing our Building Data - There for asthetics but we don't plan to move on it public Transform Building; //For simplicity we will use a cube with a collider that has the mesh renderer disabled. This will allow us to //create places for the player to walk when the player depth is different than the platform on which they //appear to be standing. public GameObject InvisiCube; //Holds our InvisiCubes so we can look at their locations or delete them private List<Transform> InvisiList = new List<Transform>(); //Keeps track of the facing direction from the last frame, helps prevent us from needlessly re-building the location //of our Invisicubes private FacingDirection lastfacing; //Keeps track of the player depth from the last frame, helps prevent us from needlessly re-building the location //of our Invisicubes private float lastDepth = 0f; //Dimensions of cubes used - so far only tested with 1. This could potentially be updated if cubes of a different //size are needed - Note: All cubes must be same size public float WorldUnits = 1.000f; // Use this for initialization void Start () { //Define our facing direction, must be the same as built in inspector //Cache our fezMove script located on the player and update our level data (create invisible cubes) facingDirection = FacingDirection.Front; fezMove = Player.GetComponent<FezMove> (); UpdateLevelData (true); } // Update is called once per frame void Update () { //Logic to control the player depth //If we're on an invisible platform, move to a physical platform, this comes in handy to make rotating possible //Try to move us to the closest platform to the camera, will help when rotating to feel more natural //If we changed anything, update our level data which pertains to our inviscubes if(!fezMove._jumping) { bool updateData = false; if(OnInvisiblePlatform()) if(MovePlayerDepthToClosestPlatform()) updateData = true; if(MoveToClosestPlatformToCamera()) updateData = true; if(updateData) UpdateLevelData(false); } //Handle Player input for rotation command if(Input.GetKeyDown(KeyCode.RightArrow)) { //If we rotate while on an invisible platform we must move to a physical platform //If we don't, then we could be standing in mid air after the rotation if(OnInvisiblePlatform()) { //MoveToClosestPlatform(); MovePlayerDepthToClosestPlatform(); } lastfacing = facingDirection; facingDirection = RotateDirectionRight(); degree-=90f; UpdateLevelData(false); fezMove.UpdateToFacingDirection(facingDirection, degree); } else if( Input.GetKeyDown(KeyCode.LeftArrow)) { if(OnInvisiblePlatform()) { //MoveToClosestPlatform(); MovePlayerDepthToClosestPlatform(); } lastfacing = facingDirection; facingDirection = RotateDirectionLeft(); degree+=90f; UpdateLevelData(false); fezMove.UpdateToFacingDirection(facingDirection, degree); } } ////// Destroy current invisible platforms /// Create new invisible platforms taking into account the /// player's facing direction and the orthographic view of the /// platforms /// private void UpdateLevelData(bool forceRebuild) { //If facing direction and depth havent changed we do not need to rebuild if(!forceRebuild) if (lastfacing == facingDirection && lastDepth == GetPlayerDepth ()) return; foreach(Transform tr in InvisiList) { //Move obsolete invisicubes out of the way and delete tr.position = Vector3.zero; Destroy(tr.gameObject); } InvisiList.Clear (); float newDepth = 0f; newDepth = GetPlayerDepth (); CreateInvisicubesAtNewDepth (newDepth); } ////// Returns true if the player is standing on an invisible platform /// private bool OnInvisiblePlatform() { foreach(Transform item in InvisiList) { if(Mathf.Abs(item.position.x - fezMove.transform.position.x) < WorldUnits && Mathf.Abs(item.position.z - fezMove.transform.position.z) < WorldUnits) if(fezMove.transform.position.y - item.position.y <= WorldUnits + 0.2f && fezMove.transform.position.y - item.position.y >0) return true; } return false; } ////// Moves the player to the closest platform with the same height to the camera /// Only supports Unity cubes of size (1x1x1) /// private bool MoveToClosestPlatformToCamera() { bool moveCloser = false; foreach(Transform item in Level) { if(facingDirection == FacingDirection.Front || facingDirection == FacingDirection.Back) { //When facing Front, find cubes that are close enough in the x position and the just below our current y value //This would have to be updated if using cubes bigger or smaller than (1,1,1) if(Mathf.Abs(item.position.x - fezMove.transform.position.x) < WorldUnits +0.1f) { if(fezMove.transform.position.y - item.position.y <= WorldUnits + 0.2f && fezMove.transform.position.y - item.position.y >0 && !fezMove._jumping) { if(facingDirection == FacingDirection.Front && item.position.z < fezMove.transform.position.z) moveCloser = true; if(facingDirection == FacingDirection.Back && item.position.z > fezMove.transform.position.z) moveCloser = true; if(moveCloser) { fezMove.transform.position = new Vector3(fezMove.transform.position.x, fezMove.transform.position.y, item.position.z); return true; } } } } else{ if(Mathf.Abs(item.position.z - fezMove.transform.position.z) < WorldUnits + 0.1f) { if(fezMove.transform.position.y - item.position.y <= WorldUnits + 0.2f && fezMove.transform.position.y - item.position.y >0 && !fezMove._jumping) { if(facingDirection == FacingDirection.Right && item.position.x > fezMove.transform.position.x) moveCloser = true; if(facingDirection == FacingDirection.Left && item.position.x < fezMove.transform.position.x) moveCloser = true; if(moveCloser) { fezMove.transform.position = new Vector3(item.position.x, fezMove.transform.position.y, fezMove.transform.position.z); return true; } } } } } return false; } ////// Looks for an invisicube in InvisiList at position 'cube' /// ////// Cube position. private bool FindTransformInvisiList(Vector3 cube) { foreach(Transform item in InvisiList) { if(item.position == cube) return true; } return false; } /// true , if transform invisi list was found,false otherwise./// Looks for a physical (visible) cube in our level data at position 'cube' /// ////// Cube. private bool FindTransformLevel(Vector3 cube) { foreach(Transform item in Level) { if(item.position == cube) return true; } return false; } /// true , if transform level was found,false otherwise./// Determines if any building cubes are between the "cube" /// and the camera /// ////// Cube. private bool FindTransformBuilding(Vector3 cube) { foreach(Transform item in Building) { if(facingDirection == FacingDirection.Front ) { if(item.position.x == cube.x && item.position.y == cube.y && item.position.z < cube.z) return true; } else if(facingDirection == FacingDirection.Back ) { if(item.position.x == cube.x && item.position.y == cube.y && item.position.z > cube.z) return true; } else if(facingDirection == FacingDirection.Right ) { if(item.position.z == cube.z && item.position.y == cube.y && item.position.x > cube.x) return true; } else { if(item.position.z == cube.z && item.position.y == cube.y && item.position.x < cube.x) return true; } } return false; } /// true , if transform building was found,false otherwise./// Moves player to closest platform with the same height /// Intended to be used when player jumps onto an invisible platform /// private bool MovePlayerDepthToClosestPlatform() { foreach(Transform item in Level) { if(facingDirection == FacingDirection.Front || facingDirection == FacingDirection.Back) { if(Mathf.Abs(item.position.x - fezMove.transform.position.x) < WorldUnits + 0.1f) if(fezMove.transform.position.y - item.position.y <= WorldUnits + 0.2f && fezMove.transform.position.y - item.position.y >0) { fezMove.transform.position = new Vector3(fezMove.transform.position.x, fezMove.transform.position.y, item.position.z); return true; } } else { if(Mathf.Abs(item.position.z - fezMove.transform.position.z) < WorldUnits + 0.1f) if(fezMove.transform.position.y - item.position.y <= WorldUnits + 0.2f && fezMove.transform.position.y - item.position.y >0) { fezMove.transform.position = new Vector3(item.position.x, fezMove.transform.position.y, fezMove.transform.position.z); return true; } } } return false; } ////// Creates an invisible cube at position /// Invisicubes are used as a place to land because our current /// depth level in 3 dimensions may not be aligned with a physical platform /// ///The invisicube. /// Position. private Transform CreateInvisicube(Vector3 position) { GameObject go = Instantiate (InvisiCube) as GameObject; go.transform.position = position; return go.transform; } ////// Creates invisible cubes for the player to move on /// if the physical cubes that make up a platform /// are on a different depth /// /// New depth. private void CreateInvisicubesAtNewDepth(float newDepth) { Vector3 tempCube = Vector3.zero; foreach(Transform child in Level) { if(facingDirection == FacingDirection.Front || facingDirection == FacingDirection.Back) { tempCube = new Vector3(child.position.x, child.position.y, newDepth); if(!FindTransformInvisiList(tempCube) && !FindTransformLevel(tempCube) && !FindTransformBuilding(child.position)) { Transform go = CreateInvisicube(tempCube); InvisiList.Add(go); } } //z and y must match a level cube else if(facingDirection == FacingDirection.Right || facingDirection == FacingDirection.Left) { tempCube = new Vector3(newDepth, child.position.y, child.position.z); if(!FindTransformInvisiList(tempCube) && !FindTransformLevel(tempCube) && !FindTransformBuilding(child.position)) { Transform go = CreateInvisicube(tempCube); InvisiList.Add(go); } } } } ////// Any actions required if player returns to start /// public void ReturnToStart() { UpdateLevelData (true); } ////// Returns the player depth. Depth is how far from or close you are to the camera /// If we're facing Front or Back, this is Z /// If we're facing Right or Left it is X /// ///The player depth. private float GetPlayerDepth() { float ClosestPoint = 0f; if(facingDirection == FacingDirection.Front || facingDirection == FacingDirection.Back) { ClosestPoint = fezMove.transform.position.z; } else if(facingDirection == FacingDirection.Right || facingDirection == FacingDirection.Left) { ClosestPoint = fezMove.transform.position.x; } return Mathf.Round(ClosestPoint); } ////// Determines the facing direction after we rotate to the right /// ///The direction right. private FacingDirection RotateDirectionRight() { int change = (int)(facingDirection); change++; //Our FacingDirection enum only has 4 states, if we go past the last state, loop to the first if (change > 3) change = 0; return (FacingDirection) (change); } ////// Determines the facing direction after we rotate to the left /// ///The direction left. private FacingDirection RotateDirectionLeft() { int change = (int)(facingDirection); change--; //Our FacingDirection enum only has 4 states, if we go below the first, go to the last state if (change < 0) change = 3; return (FacingDirection) (change); } } //Used frequently to keep track of the orientation of our player and camera public enum FacingDirection { Front = 0, Right = 1, Back = 2, Left = 3 }
When we start the game, the script will use the player's location to determine at what depth to create the invisible platforms composed of our invisicubes. We've added the player FacingDirection enum at the bottom of the script. This will track which direction the player is currently facing. When the player is facing Front, or Back, the Z position controls depth. When facing Right or Left, the depth will be the X position. Each time the player presses the Right or Left arrow on the keyboard, we will rotate the entire Player GameObject in the appropriate direction by 90 degrees. Because we have our Camera parented to the Player GameObject, it comes along for the rotation as well. This is convenient because it keeps our sprite character facing the camera the entire time.
In the Update function we will begin by making sure the player is not jumping, this is to ensure it's safe for us to change the player's depth. If we're on an invisible platform, we know that at least two of the coordinates of the player's position will match that of one of the cubes that makes up a platform. For example, if our facingDirection is Front, the X position of the player when rounded is the same as at least one of the cubes that make up a physical platform. The Y coordinate must also be the same as one of the cubes, but offset roughly by the size of the Character Controller. The Z coordinate is the main difference, it could be either closer or further from the camera.
Reading through the commented code may provide better insight into what is happening. Next, we will need to add something to our FezMove script to handle the rotation command. We will create a setter function for the FacingDirection of the player and check the facing direction during our MoveCharacter function to determine which direction we should be moving in now that we can rotate in 3D. Here is the updated script, you can copy this in place of the current FezMove script we wrote in Part 2 of this tutorial.
using UnityEngine; using System.Collections; public class FezMove : MonoBehaviour { private int Horizontal = 0; public Animator anim; public float MovementSpeed = 5f; public float Gravity = 1f; public CharacterController charController; private FacingDirection _myFacingDirection; public float JumpHeight = 0f; public bool _jumping = false; private float degree = 0; public FacingDirection CmdFacingDirection { set{ _myFacingDirection = value; } } // Update is called once per frame void Update () { if (Input.GetAxis ("Horizontal") < 0) Horizontal = -1; else if (Input.GetAxis ("Horizontal") > 0) Horizontal = 1; else Horizontal = 0; if (Input.GetKeyDown (KeyCode.Space) && !_jumping) { _jumping = true; StartCoroutine(JumpingWait()); } if(anim) { anim.SetInteger("Horizontal", Horizontal); float moveFactor = MovementSpeed * Time.deltaTime * 10f; MoveCharacter(moveFactor); } transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.Euler(0, degree, 0), 8 * Time.deltaTime); } private void MoveCharacter(float moveFactor) { Vector3 trans = Vector3.zero; if(_myFacingDirection == FacingDirection.Front) { trans = new Vector3(Horizontal* moveFactor, -Gravity * moveFactor, 0f); } else if(_myFacingDirection == FacingDirection.Right) { trans = new Vector3(0f, -Gravity * moveFactor, Horizontal* moveFactor); } else if(_myFacingDirection == FacingDirection.Back) { trans = new Vector3(-Horizontal* moveFactor, -Gravity * moveFactor, 0f); } else if(_myFacingDirection == FacingDirection.Left) { trans = new Vector3(0f, -Gravity * moveFactor, -Horizontal* moveFactor); } if(_jumping) { transform.Translate( Vector3.up * JumpHeight * Time.deltaTime); } charController.SimpleMove (trans); } public void UpdateToFacingDirection(FacingDirection newDirection, float angle) { _myFacingDirection = newDirection; degree = angle; } public IEnumerator JumpingWait() { yield return new WaitForSeconds (0.35f); //Debug.Log ("Returned jump to false"); _jumping = false; } }
To use this script, create a new empty GameObject and attach the script. You will need to drag and drop the appropriate GameObjects from our scene into the FezManager fields. In the Level field, add the Platforms GameObject and the Buildings GameObject in the Building field. You will need to drag the Invisicube from the assets folder onto it's appropriate field as well.
Your FezManager GameObject should look something like this
Platforms and Buildings
The layout of the level is ultimately up to you, however, there are some guidelines that should be observed. As you build the level and test it, you'll find some configurations work better than others.
When building your level, remember to place the cubes used to build platforms under the Platforms GameObject and the rest under Buildings
Start by building a level consisting of a central stone Building and surround it with some Platforms, don't forget to use our AutoSnap script from the first tutorial for placing cubes
Mid-rotation
That's all for this tutorial, thanks for reading!
How did your game turn out? Please share any suggestions or issues you find along the way by commenting below.
I loved your post and tutorials. Would it be possible for you to post the final folder? I don't know what I've been doing wrong but I'm not able to get a nice movement with the character and I'm wondering what I've done bad.
ReplyDeleteSure, you can download the project here: https://github.com/gmatsura/gmatsura-UnityFezLike
ReplyDeleteThank you so much! This really helped me a lot :)
ReplyDeleteCan you switch the horizontal movement in the fezmove to touch screen if so, how?
ReplyDeleteReally impressive. I'm following your posts and got in trouble.
ReplyDelete1. without jump, character can't go to another platform
2. charcter can jump multiple times.
and i found your git sources are different version from your vids one.
can you send that vids version source to escandar2836@gmail.com?
Thanks.
What actually is the code for moving the camera because I made my own movement and just need the camera move part.
ReplyDelete