Showing posts with label Grid Framework. Show all posts
Showing posts with label Grid Framework. Show all posts

Sunday, July 28, 2013

Grid Based Movement in Unity

If you have played old school games like Dungeon Master, Eye of the Beholder, the early Lands of Lore and Wizardry series or even more recent games like Legend of Grimrock, then you know what Grid Based Movement is.    This is my implementation of grid based movement using Grid Framework by HiPhish.  Feel free to use your own grid system.

See script in action

Setup a scene and if you are using Grid Framework create a new grid.
Add an empty gameobject to represent the player.  I also make the camera a child of this object and set the camera position to 0,0,0 so it is always looking forward from the centre of the player.

Add this script to the player object.  If you are not using Grid Framework, you will need to change/remove the GFGrid variable and the first line in the awake method.  All this line currently does is move the player to 0,0,0.  In a later post the grid will be used to determine if a move is valid or not.  You will also need to either create the inputs I have specified in the script or update the script to match your own.  I use w,s,a,d for forwards, backward, stepleft, stepright and q and e for turning.

public GFGrid Grid;//Grid the Player is attached to.
public float StepSpeed = 0.5f; //how fast in seconds each step or turn takes to complete
public Vector3 CurrentGridPosition = new Vector3(0,0,0);  //3d position in Grid
public string CurrentDirection; //Direction Player is facing (n,s,e,w)

float timer = 0; //used to disable turn left and turn right inputs when player is already turning
bool enableRotation = true; //used to disable turn left and turn right inputs when player is already turning
Vector3 intendedPosition;//Before player is moved, the position the player will end up at is stored here
//Is used to check that the move is valid and also used to disable move inputs
//when player is already moving

void Awake()
{
this.transform.position = Grid.GridToWorld(CurrentGridPosition); //move player to default position/grid origin
intendedPosition = CurrentGridPosition; //no movement has occured yet, so intended position is set to current position
CurrentDirection = "n"; //TODO: default direction is calculated from CurrentGridPosition.
}

void Start()
{
//sets timer to match time it takes to make one step or turn
timer = StepSpeed; 
}

public void MoveTo(Vector3 position)
{
//Move Player to position over time
LeanTween.move(gameObject, position, StepSpeed);
}

public void TurnPlayer(Vector3 position)
{
//Turn Player to new rotation over time
LeanTween.rotate(gameObject, position, StepSpeed);

}

#region Movement

public void MoveForward()
{
// calculate intended position. Get current direction and add one step forward
switch(CurrentDirection.ToLower())
{
case "n":
default:
{
intendedPosition = CurrentGridPosition + new Vector3(0,0,1);
break;
}
case "e":
{
intendedPosition = CurrentGridPosition + new Vector3(1,0,0);
break;
}
case "s":
{
intendedPosition = CurrentGridPosition + new Vector3(0,0,-1);
break;
}
case "w":
{
intendedPosition = CurrentGridPosition + new Vector3(-1,0,0);
break;
}
}

//TODO: Check move is valid, if not, break
MoveTo(Grid.GridToWorld(intendedPosition)); //Actually move player
CurrentGridPosition = intendedPosition; //Update CurrentGridPosition
}

public void MoveBackward()
{
// calculate intended position. Get current direction and add one step backward
switch(CurrentDirection.ToLower())
{
case "n":
default:
{
intendedPosition = CurrentGridPosition + new Vector3(0,0,-1);
break;
}
case "e":
{
intendedPosition = CurrentGridPosition + new Vector3(-1,0,0);
break;
}
case "s":
{
intendedPosition = CurrentGridPosition + new Vector3(0,0,1);
break;
}
case "w":
{
intendedPosition = CurrentGridPosition + new Vector3(1,0,0);
break;
}
}
//TODO: Check move is valid
MoveTo(Grid.GridToWorld(intendedPosition)); //Actually move player
CurrentGridPosition = intendedPosition; //Update CurrentGridPosition
}

public void StepLeft()
{
// calculate intended position. Get current direction and add one step left
switch(CurrentDirection.ToLower())
{
case "n":
default:
{
intendedPosition = CurrentGridPosition + new Vector3(-1,0,0);
break;
}
case "e":
{
intendedPosition = CurrentGridPosition + new Vector3(0,0,1);
break;
}
case "s":
{
intendedPosition = CurrentGridPosition + new Vector3(1,0,0);
break;
}
case "w":
{
intendedPosition = CurrentGridPosition + new Vector3(0,0,-1);
break;
}
}
//TODO: Check move is valid
MoveTo(Grid.GridToWorld(intendedPosition));//Actually move player
CurrentGridPosition = intendedPosition; //Update CurrentGridPosition
}

public void StepRight()
{
// calculate intended position. Get current direction and add one step right
switch(CurrentDirection.ToLower())
{
case "n":
default:
{
intendedPosition = CurrentGridPosition + new Vector3(1,0,0);
break;
}
case "e":
{
intendedPosition = CurrentGridPosition + new Vector3(0,0,-1);
break;
}
case "s":
{
intendedPosition = CurrentGridPosition + new Vector3(-1,0,0);
break;
}
case "w":
{
intendedPosition = CurrentGridPosition + new Vector3(0,0,1);
break;
}
}

//TODO: Check move is valid
MoveTo(Grid.GridToWorld(intendedPosition)); //Actually move player
CurrentGridPosition = intendedPosition; //Update CurrentGridPosition
}

public void TurnLeft()
{
//check if player is already turning
if(!enableRotation)
return;

enableRotation = false; //disable turn inputs

Vector3 intendedPosition = transform.localEulerAngles + new Vector3(0,-90,0);//calculate intended position after turning 

TurnPlayer(intendedPosition); //actually turn player

//Because player has turned, player direction has changed.  This updates to correct value
switch (CurrentDirection.ToLower())
{
default:
case "n":
CurrentDirection = "w";
break;
case "e":
CurrentDirection = "n";
break;
case "s":
CurrentDirection = "e";
break;
case "w":
CurrentDirection = "s";
break;
}
}

public void TurnRight()
{
//check if player is already turning
if(!enableRotation)
return;

enableRotation = false; //disable turn inputs

Vector3 intendedPosition = transform.localEulerAngles + new Vector3(0,90,0);//calculate intended position after turning 

TurnPlayer(intendedPosition); //actually turn player

//Because player has turned, player direction has changed.  This updates to correct value
switch (CurrentDirection.ToLower())
{
default:
case "n":
CurrentDirection = "e";
break;
case "e":
CurrentDirection = "s";
break;
case "s":
CurrentDirection = "w";
break;
case "w":
CurrentDirection = "n";
break;
}
}

#endregion

void Update()
{
//checks if player is moving, if not enables movement inputs
if(CurrentGridPosition == intendedPosition)
{
if(Input.GetButtonDown("Forward"))
{
MoveForward();
}
if(Input.GetButtonDown("Backward"))
{
MoveBackward();
}
if(Input.GetButtonDown("StepLeft"))
{
StepLeft();
}
if(Input.GetButtonDown("StepRight"))
{
StepRight();
}
}

//checks if player is turning, if not enables turning inputs
if(enableRotation)
{
if(Input.GetButtonDown("TurnLeft"))
{
TurnLeft();
}
if(Input.GetButtonDown("TurnRight"))
{
TurnRight();
}
}

//if player is turning, checks if turn has finished.
if(!enableRotation)
{
timer -= Time.deltaTime;
if(timer < 0)
{
enableRotation = true;
timer = StepSpeed;

}
}
}





Monday, July 22, 2013

Using Grid Framework to build a platformer

The last months have been insanely busy.  I may have overdone it on my still healing foot, and so today I find myself at home and forced to rest.  It sounds like a perfect opportunity to make a game AND a tutorial!!

I enlisted the help of my 4 year old daughter in deciding what kind of game to make.  She wanted a game where Happy Face has to collect a crown.  Happy Face will encounter a witch, a Mother Spider and her Baby Spiders.  So, I am going to be building a single screen platformer.  By single screen, I mean that what is displayed is the entire game, no scrolling, no extra rooms.  At least not yet :)

I have also just purchased Grid Framework and this is a great time to see what it can do.

First thing is to create the actual grid.  GameObject > Create Grid > Rectangular Grid will do the trick.  Hexagonal Grids and Polar Grids are also available.  Hexagonal grids will be perfect for deep strategy games.  A polar grid is a circular grid, and the only use I can think of for it right now is as some kind of radar or motion sensor display.

Grid with Default Settings

Now to shape the grid to fit our game world.  For this game, I need a grid twice as wide as it is tall.  I dont care about depth, so I set the Z to 0.  X is 12 and Y is 5.  Your dimensions may vary.

2D Grid
I should now be able to use this grid to create a basic level layout.  Open up the Grid Align Panel (Window > Grid Align Panel) and turn on AutoSnapping.  Make sure you have assigned your grid to the panel too.
Create a basic cube (GameObject > Create Other > Cube) and drag it onto the grid.  You will see that it is perfectly aligned, and moving it around will snap it to the grid.  Place a bunch of these and you have a level.
I will create a boundary first, to stop an eager 4 year old from jumping off the world into oblivion.

Once the boundary is created, lets get Happy Face into the game and moving.   All artwork has been provided by my daughter :)

For the sake of simplicity, I am faking a 2D sprite by creating a cube with a transparent diffuse and her painting attached.  Also add a character controller.  This will replace the default box collider.

Fake2D Sprite settings
Lets add some simple scripting to get this happy fella moving.  Attach this script to your 'sprite'

float moveSpeed = 3.0f;
float jumpSpeed = 8.0f;
float gravity = 10.0f;
Vector3 moveDirection = Vector3.zero;
// Update is called once per frame
void Update ()
{
//handle input and move character controller;
CharacterController cc = GetComponent<CharacterController>();
if(cc.isGrounded)
{ moveDirection = new Vector3(Input.GetAxis("Horizontal"), 0, 0); // Gets Horizontal movement. Edit > Project Settings > Input to find out more about Axis.
moveDirection = transform.TransformDirection(moveDirection);
moveDirection *= moveSpeed;
if(Input.GetButton("Jump") && cc.isGrounded)
{
moveDirection.y = jumpSpeed; }
}
moveDirection.y -= gravity * Time.deltaTime;
cc.Move(moveDirection * Time.deltaTime);


Test it out and you should be able to walk your sprite back and forth without it falling out of the world.  Lets add a few platforms and the crown.  The grid will allow me to easily line up platforms, and with the current gravity settings on the player I know that the sprite can jump just over 3 grid squares high, so I need a maximum distance of 3 squares floor to floor for the character to be able to make the jump.

Basic level


Lets add a quick script to the crown to allow it be 'collected'.
When the player touches the crown, the following needs to happen:
Record that the crown has been collected
Play a 'pick up' sound
'Destroy' the game object.

To make a trigger on the crown object, I select it and set 'Is Trigger' to true on the box collider.  I also have an empty object in my scene called 'Game Manager', with a game manager script attached.  I'll use this to track any information that the entire game needs to know about.  Right now it just consists of a single public bool to track if the crown has been collected or not.  Set it's default value to false (uncollected).

My crown script.  I used the free tool SFXR to generate my sound fx.

using UnityEngine;
using System.Collections;

public class Crown : MonoBehaviour {

void OnTriggerEnter(Collider other) //fired when an object ENTERS the trigger zone
{
//Record that crown has been collected
GameManager gmScript = GameObject.Find("Game Manager").GetComponent<GameManager>();
gmScript.CrownCollected = true;

//Play Pick up sound
audio.Play();

//Destroy Object
Destroy(GameObject.Find("Crown"), 0.3f);
}
}

Test and you should have a playable level with a pick upable crown.  Whats missing?  Friends, of course!!

Next part coming soon....