Isometric tile-based RPG: Restricting turns to 45 degrees?

I’m creating an isometric RPG-style game, and the world is tile-based (so one character per tile, etc.). Think Jagged Alliance 2, X-Com or whatever.

As common with these games, characters can only look in one of 8 viewing directions (fwd, back, left, right, and the diagionals).

When I click my mouse button somewhere on the terrain around the character, he should turn towards it smoothly (not instantly) but only until he reaches a rotation that is the nearest 45 degree angle (for 8 possible rotations) for the point I clicked at.

I have trouble working with Quaternions so I took the cumbersome approach: Have 8 seperate colliding planes around the character in an octagonal fashion, and then cast a ray out from the chracter towards the point I clicked. Depending on the plane that was hit by the ray, the Vector3 coordinates of the final point that the character should rotate towards (one of the eight tiles around him) are assigned.

The system only works kind of, though. When first ordered to turn, the Vector3 isn’t updated, only if I click again, and the same hold true after reaching a new waypoint. (-> “Look rotation viewing vector is zero” error)

The question is, how can I make the turning mechanic more simple?

var MouseCoords : Vector3;
var LookTarget : Vector3;
var TurnSpeed : float = 3.0;
var IsTurning : boolean;

function Update()
{
     if (Physics.Raycast (ray, hit, Mathf.Infinity, TerrainMask))
     {
          MouseCoords = Vector3( Mathf.RoundToInt(hit.point.x),  Mathf.RoundToInt(hit.point.y),  Mathf.RoundToInt(hit.point.z));
     }


     if(Input.GetButtonDown("Fire1"))
     {
          CheckRotation();
          TurnTowards(LookTarget);
     }
}

function CheckRotation()
{
	var RotationTargetX : float = transform.position.x;
	var RotationTargetZ : float = transform.position.z;
	
	var LocalOffset : Vector3 = transform.position + transform.up * 0.5;
	var TurnTarget : Vector3 = Vector3(MouseCoords.x, transform.position.y, MouseCoords.z);
	var hit : RaycastHit;
	
	if(Physics.Raycast (transform.position, TurnTarget - transform.position, hit, Mathf.Infinity, RotationMask))
	{
		if (hit.collider.gameObject.CompareTag("RotationMeshN"))
		{
			RotationTargetZ += 1.0;
		}
		if (hit.collider.gameObject.CompareTag("RotationMeshNE"))
		{
			RotationTargetX += 1.0;
			RotationTargetZ += 1.0;
		}
		if (hit.collider.gameObject.CompareTag("RotationMeshE"))
		{
			RotationTargetX += 1.0;
		}
		if (hit.collider.gameObject.CompareTag("RotationMeshSE"))
		{
			RotationTargetX += 1.0;
			RotationTargetZ -= 1.0;
		}
		if (hit.collider.gameObject.CompareTag("RotationMeshS"))
		{
			RotationTargetZ -= 1.0;
		}
		if (hit.collider.gameObject.CompareTag("RotationMeshSW"))
		{
			RotationTargetX -= 1.0;
			RotationTargetZ -= 1.0;
		}
		if (hit.collider.gameObject.CompareTag("RotationMeshW"))
		{
			RotationTargetX -= 1.0;
		}
		if (hit.collider.gameObject.CompareTag("RotationMeshNW"))
		{
			RotationTargetX -= 1.0;
			RotationTargetZ += 1.0;
		}
	}

	LookTarget = Vector3(RotationTargetX, transform.position.y, RotationTargetZ);
	TurnTowards(LookTarget);
}

function TurnTowards(LookAtTarget : Vector3)
{
     if(IsTurning == false)
     {
          var dir : Vector3 = (LookAtTarget - transform.position).normalized;
	  var NewRotation = Quaternion.LookRotation(dir);	
 
	  for (u = 0.0; u <= 1.0; u += (TurnSpeed * Time.deltaTime)) 
	  {
	       IsTurning = true;
	       //transform.LookAt(LookAtTarget); // for instant turns
	        
	       if(LookAtTarget != transform.position)
	       {
    	            transform.rotation = Quaternion.Slerp( transform.rotation, Quaternion.LookRotation(LookAtTarget - transform.position), TurnSpeed);
   	       } 
	        yield;
	  }
	    
	  IsTurning = false;
     }
}

I had a hard time sorting out your code, so I’m going to give you some example code instead:

#pragma strict 

var speed = 90.0;
private var qTo : Quaternion;

function Update() {
     if(Input.GetButtonDown("Fire1")) {
          NewTurn();
     }
     transform.rotation = Quaternion.RotateTowards(transform.rotation, qTo, speed * Time.deltaTime);
}
 
function NewTurn () {
	var hit : RaycastHit;
	var ray = Camera.main.ScreenPointToRay(Input.mousePosition);

	if(Physics.Raycast (ray, hit)) {
		var aimingDirection = hit.point - transform.position;
		var angle = -Mathf.Atan2(aimingDirection.z, aimingDirection.x) * Mathf.Rad2Deg + 90.0;
		angle = Mathf.Round(angle / 45.0f) * 45.0f;
		qTo = Quaternion.AngleAxis(angle, Vector3.up);
	}
}

If you want a more eased movement to the turn, replace Quaternion.RotateTowards() with Quaternion.Slerp() and adjust the speed way down.

I finally managed to get it to work, seems like I made another mistake and for some reason typed in “Lerp” instead of “Slerp” :slight_smile:

Here is the working code that has everything but the initial mouseclick and function call in a seperate function outside Update():

    var speed = 90.0;
    private var qTo : Quaternion;
    var IsTurning : boolean;
    var t : float;
     
function Update() 
{
	if(Input.GetButtonDown("Fire1")) 
	{
		NewTurn();
	}

}
     
function NewTurn () 
{
	if(IsTurning == false)
	{
	    var hit : RaycastHit;
	    var ray = Camera.main.ScreenPointToRay(Input.mousePosition);
	     
	    if(Physics.Raycast (ray, hit)) 
	    {
		    var aimingDirection = hit.point - transform.position;
		    var angle = -Mathf.Atan2(aimingDirection.z, aimingDirection.x) * Mathf.Rad2Deg + 90.0;
		    angle = Mathf.Round(angle / 45.0f) * 45.0f;
		    qTo = Quaternion.AngleAxis(angle, Vector3.up);
		    print(qTo);
		    for (t = 0.0; t <= 1.0; t += Time.deltaTime * speed) 
			{
				IsTurning = true;
		    	transform.rotation = Quaternion.Slerp(transform.rotation, qTo, t);
		    	yield;
		    }
		    IsTurning = false;
	    }
	}
}