Third Person Click-To-Move / Interaction system

Hello Unity Community!

I’m hoping you guys can help me out with a script I’m working on. Basically, it’s a top down, 2D movement system (Click to move) that I want to tie into object interaction. This is one of the first scripts I’m working with on this project, and already am running into major trouble. Granted, my scripting background is sparse at best, so I’m hoping some of your more experienced individuals can help me out!

Objective of script:
Player clicks on the screen and grabs a location. This is done using a Raycast function that returns the object to a RaycastHit variable. Depending on a few variables, a series of interactions fires off at this point:
If the object is terrain, or unmarked (road, titled gradient,etc), then move the character directly from current location to target location (hit.point, etc) within a set distance (using a Vector3.Distance check of a small amount. The character (a cube in this case) needs to rotate to face the object before moving, or within short distance of starting. If the object it clicks on returns a “Harvestable” or “Container” tag, then the character moves to the object within a distance, and then executes the code to open the container or harvest the harvestable (not even thinking of these scripts at this point, but getting the placeholders set). If the object returns an “Enemy” tag, then move to the charcters weaponRange variable, and then execute cWeaponAttack script (basic scripts that would be written at a later date to do some math functions). If the object returns NPC, the character would move to the location, then when arriving, execute an openDialogue function (again, to be written later).

At this point, that’s all I want it to do.

TLDR:
Looking for help making a script. My attempts so far have not worked, so my framework is pretty much void at this point. Looking for a script to do:
Player = primitive Cube
Player clicks on Terrain → Cube rotates and moves to location of the raycast intersect.
Player clicks on Object/Harvestable → Cube rotates and moves near to the object, then fires off a script
Player clicks on Enemy → Cube rotates and moves within range of a stored weaponRange variable, then fires off a script
Player clicks on NPC → Cube rotates and moves to target NPC, then fires off a script.
Not looking for help (right now, heh) on any of the sub-scripts; just the movement script. All my attempts thus far have not worked properly (Cube doesn’t rotate/Cube only moves when clicking/Cube spins in place/etc, etc).

Working with a basic Terrain, standard cube, and the associated colliders. I’d post what I have but frankly, there are so many “let me see if this works” changes that aren’t documented that it will most likely have to be scrapped. All it does so far is make a ray in Camera.main.ScreenPointToRay(Input.mousePosition), then make a RaycastHit ‘hit’, does the basic Physics.RaycastHit check, then compares the tags and moves from there. I had the script storing a boolean isMoving that would then loop in Update on the transform.translates of a dir I made from transform.position - hit.position normalized and such… but nothing at all has worked.

Thanks in advance and I’ll do my best to return any more required info.

Greetings. You’ve provided alot of information, although its a bit overwelming. As for the movement script, im assuming you want to click someplace on the floor and have the character move there right? Of the top of my head, when the player clicks you could instantiate a gameObject there and have the character move towards it. Then once it gets there, delete it. Or if the player clicks someplace and then clicks somewhere else before the character gets to the first destination, the character could change targets and the last destination could get deleted.

Just start coding the script and post any problems you have with it that you cant get around.

Here’s an example of one of the ones I tried. This one has some statics in place that shouldn’t be there, as well as rotational issues, and the speed is never correct.

#pragma strict

var moveSpeed : float = 1.0;
var turnSpeed : float = 5.0;
var locationToMoveTo : Vector3;
var movePoint : Transform;
var dir : Vector3;
var isMoving : boolean = false;
var bullet : Transform;
var isTurned : boolean = false;
var objectToTravelTo : Transform;
var pointAvailable : boolean = false;

private var lookRotation : Quaternion;


function Start () {

}

function Update () {
	//Mouse Location awarenesss. When Mouse-Left is pressed, then...
	if(Input.GetMouseButtonDown(0))	{
		var ray : Ray = Camera.main.ScreenPointToRay(Input.mousePosition);
		var hit : RaycastHit;
		//Casts a ray, stores the location and object it transected in Hit
		if(Physics.Raycast(ray,hit,100))	{
			//If it hits an Enemy-
			if(hit.transform.tag == "Enemy")	{
				print("Enemy Hit!");
			}
			//If it hits a Harvestable-
			if(hit.transform.tag == "Harvestable")	{
				
			}
			//If it hits a Container
			if(hit.transform.tag == "Container")	{
				
			}
			//If it hits a NPC
			if(hit.transform.tag == "NPC")	{
				
			}
			//If it is something else or Terrain...
			else	{
				var currentPoint = Instantiate(objectToTravelTo, hit.point, transform.rotation);
				//Debug print of the instantiated objects location (currentPoint)
				print("currentPoint location (Moving to): " + currentPoint.transform.position);
				var relativePos = currentPoint.transform.position - transform.position;
	  			var rotation = Quaternion.LookRotation(relativePos);
				locationToMoveTo = Vector3(hit.point.x, hit.point.y + 1, hit.point.z);
				isMoving = true;
				isTurned = false;
			}
		}
	}
	
	if(isMoving)	{
		
		dir = (hit.point -  transform.position).normalized;
		lookRotation = Quaternion.LookRotation(dir);
		transform.position = Vector3.Lerp(transform.position, locationToMoveTo, moveSpeed * Time.deltaTime);
		if(transform.rotation != lookRotation)	{
			transform.rotation = Quaternion.Slerp(transform.rotation, lookRotation, turnSpeed * Time.deltaTime);
		}
		else	{
			isTurned = true;
		}
		print("Moving...");
		print("The distance current is: " + Vector3.Distance(transform.position, locationToMoveTo));
		if(Vector3.Distance(transform.position, locationToMoveTo) < .25)	{	
			print("I've arrived!");
			isMoving = false;
		}
	}
}

heres a few questions

where does this script attach to, the character? Does the character at least move toward the area that the player clicked?

Whats the game about? Like how it plays and what the objective is.

It’s a top down, stat heavy third person RPG. It draws conceptually from the first two Fallouts, Arcanum, and Planescape;Torment. Right now, I’m getting down the movement system so we can start the conversion of game mechanics into a demo.

At the moment, the cube will move to the location clicked without incident. The two major issues are that:

A) The cubes movement speed is not effected by the move variable; it’s a consistent, very fast speed.

B) The cube does not rotate in any way. It does “flash” a bit and seem to turn, but always orients itself in the wrong direction.

If I can get those ironed out, then implementation on the moveTo functions on the distance variables could begin (IE, when the Vector3.Distance between the player transform and the enemy is equal to the weaponRange variable, then execute the combat scripts, etc etc).

After playing with the script for a bit longer, I think I’ve come to realize I don’t quite have a grasp on exactly how the * Time.deltaTime functions work. I was under the impression that any open function (IE, If statements met) that had Time.deltaTime would always be restricted by that, but that’s simply not the case in the my code. Also the rotation script seems to be the same way…

Does anyone have a basic script that rotate a cube towards where you click on terrain? Or, without rotating, just LERPs a cube from the current point to another in an actual Time.deltaTime based fashion? I can do it on awakes, or with static variables, but something just isn’t clicking (no pun intended) on this one…

Almost there!

So now the cube will rotate towards the location (using an empty game object tagged with a move label) at the correct speed, and rotates towards it as well…

However now I’m running into the odd issue where if the character is allowed to arrive at the location before clicking again, it takes two clicks to get him moving…

IE, if I click the top right corner and he moves there, if I click somewhere else half way, he’ll immediately turn and go that direction; however, if I let him get to the top right corner then click somewhere else, he simply turns a half turn in place and remains still. A second click gets him moving again.

Odd.

Maybe someone more experienced sees something obvious.

#pragma strict

var movePoint : Transform;

var turnSpeed : float = 10.0;
var moveSpeed : float = 1;

private var isMoving : boolean = false;

private var currentMovePoint : Transform;

function Start () {

}

function Update () {

	
	
	if(Input.GetMouseButtonDown(0))	{
		var ray : Ray = Camera.main.ScreenPointToRay(Input.mousePosition);
		var hit : RaycastHit;
		
		if(Physics.Raycast(ray,hit,100))	{
			print("We got a hit at " + hit.point + "!");
			var toKill = GameObject.FindGameObjectWithTag("MoveLocation");
			Destroy(toKill);
			Instantiate(movePoint, Vector3(hit.point.x, hit.point.y + 1, hit.point.z), transform.rotation);
			isMoving = true;
		}
	}
	
	if(isMoving)	{
		var toMove = GameObject.FindGameObjectWithTag("MoveLocation");
		transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(Vector3(toMove.transform.position.x,toMove.transform.position.y,toMove.transform.position.z)  - Vector3(transform.position.x,transform.position.y,transform.position.z) ), turnSpeed * Time.deltaTime);
		transform.position = Vector3.Lerp(transform.position, Vector3(toMove.transform.position.x, toMove.transform.position.y, toMove.transform.position.z) , moveSpeed * Time.deltaTime);
		if(Vector3.Distance(transform.position, toMove.transform.position) <= 1)	{
			//isMoving = false;
			print("I'm here.");
		}
	}
}

If I recall correctly Time.deltaTime only really has significance in either Update() or FixedUpdate() since it represents the amount of time that has passed since the last frame/fixed frame.

Here’s a more significant question, why are you using a Linear Interpolation(Lerp)? My understanding is that you want to restrict movement speed, but I think you missed the fact that the last input for a Lerp function is a percentage value (being from 0 to 1). Since deltaTime is usually a very small number your function still kinda works, but with anything more than 1 Lerp wouldn’t have any effect at all.

Take this as an example:

transform.position = Vector3.Lerp(transform.position, locationToMoveTo, moveSpeed * Time.deltaTime);

Assuming moveSpeed is a value of 10, and deltaTime gave you a value of 0.01 (the last frame took 1/100th of a second), this would mean your transform Lerps 0.1, or 10% the distance from itself to the target every frame.

If you want this transform to move at a rate of 10 units every second however, you would just move the transform directly:

transform.position = transform.position + ( ( locationToMoveTo- transform.position ) * moveSpeed * Time.deltaTime );

Or, I believe this handy method should work

transform.position = Vector3.MoveTowards( transform.position, locationToMoveTo, moveSpeed * Time.deltaTime );

The last parameter for MoveTowards restricts translation magnitude like you want.

Ahh, that makes sense. I was using LERP because the material I found on moving objects recommended using that… Your option seems much better.

Just about to add it all now, but the script is coming together nicely! Going to convert out the LERPs and such coming up, but here’s where we are so far.

Right now, it’s working great except for the Lerp hangups and this really odd bug where the Player refuses to move on the click directly after arriving…

It’s almost like the engine is lagging for a fraction of a second after Instantiating the new movepoint after isMoving has been switched to False… Hm.

#pragma strict

var turnSpeed : float = 10.0;
var moveSpeed : float = 1;
var currentWeaponRange : float = 30.0;
var currentHP : int = 30;
var maxHP : int = 30;

var locationToMoveTo : Vector3;
var dir : Vector3;

//Transforms
var movePoint : Transform;
var bullet : Transform;
var currentTarget : Transform = null;
private var currentMovePoint : Transform;
private var theBarrel : GameObject;

var currentWeaponMinDamage : int = 5;
var currentWeaponMaxDamage : int = 15;

private var lookRotation : Quaternion;

var testEnemy = Enemy("TestEnemy", 30);

var targetAquired : boolean = false;
var isMovingToAttack : boolean = false;
var pointAvailable : boolean = false;

private var isMoving : boolean = false;



private var toKill : GameObject;

var hit : RaycastHit;
var ray : Ray;

var enemyScript : scriptEnemyAttacker = null;

function Start () {
	theBarrel = GameObject.FindGameObjectWithTag("theBarrel");
}

function Update () {
	//Mouse Location awarenesss. When Mouse-Left is pressed, then...
	if(Input.GetMouseButtonDown(0))	{
		ray = Camera.main.ScreenPointToRay(Input.mousePosition);
		//Casts a ray, stores the location and object it transected in Hit
		if(Physics.Raycast(ray,hit,1000))	{
			//If it hits an Enemy-
			if(hit.transform.tag == "Enemy")	{
				currentTarget = hit.transform;
				enemyScript = hit.transform.GetComponent(scriptEnemyAttacker);
				toKill = GameObject.FindGameObjectWithTag("MoveLocation");
				Destroy(toKill);
				Instantiate(movePoint, Vector3(hit.point.x, hit.point.y + 1, hit.point.z), transform.rotation);
				if(Vector3.Distance(transform.position, hit.transform.position) <= currentWeaponRange)	{
					//Fire the Bullet
					fireBullet();
					var theDamage = Random.Range(currentWeaponMinDamage, currentWeaponMaxDamage);
					enemyScript.dealDamage(theDamage);
					print("Enemy currently is " + enemyScript.curHP + "/" + enemyScript.maxHP);
					targetAquired = true;
					currentTarget = hit.transform;
				}
				else	{
					print("Moving to attack...");
					isMovingToAttack = true;
				}
			}
			//If it hits a Harvestable-
			if(hit.transform.tag == "Harvestable")	{
				
			}
			//If it hits a Container
			if(hit.transform.tag == "Container")	{
				
			}
			//If it hits a NPC
			if(hit.transform.tag == "NPC")	{
				
			}
			//If it is something else or Terrain...
			if(hit.transform.tag == "Terrain")	{
				print("We got a hit at " + hit.point + "!");
				toKill = GameObject.FindGameObjectWithTag("MoveLocation");
				Destroy(toKill);
				Instantiate(movePoint, Vector3(hit.point.x, hit.point.y + 1, hit.point.z), transform.rotation);
				isMoving = true;
			}
		}
	}
	
	if(isMoving)	{
		var toMove = GameObject.FindGameObjectWithTag("MoveLocation");
		if(Vector3.Distance(transform.position, toMove.transform.position) <= 1)	{
			isMoving = false;
			print("I'm here.");
		}
		else	{
			transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(Vector3(toMove.transform.position.x,toMove.transform.position.y,toMove.transform.position.z)  - Vector3(transform.position.x,transform.position.y,transform.position.z) ), turnSpeed * Time.deltaTime);
			transform.position = Vector3.Lerp(transform.position, Vector3(toMove.transform.position.x, toMove.transform.position.y, toMove.transform.position.z) , moveSpeed * Time.deltaTime);
		}
	}
	if(isMovingToAttack)	{
		isMoving = false;
		var toMoveToAttack = GameObject.FindGameObjectWithTag("MoveLocation");
		if(Vector3.Distance(transform.position, hit.transform.position) <= currentWeaponRange)	{
			print("Current range is: " + Vector3.Distance(transform.position, toMoveToAttack.transform.position) + ". My range is " + currentWeaponRange);
			fireBullet();
			theDamage = Random.Range(currentWeaponMinDamage, currentWeaponMaxDamage);
			enemyScript.dealDamage(theDamage);
			print("Enemy currently is " + enemyScript.curHP + "/" + enemyScript.maxHP);
			targetAquired = true;
			isMovingToAttack = false;
			print("I'm here after attack move and attack.");
		}
		else	{
			transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(Vector3(toMoveToAttack.transform.position.x,toMoveToAttack.transform.position.y,toMoveToAttack.transform.position.z)  - Vector3(transform.position.x,transform.position.y,transform.position.z) ), turnSpeed * Time.deltaTime);
			transform.position = Vector3.Lerp(transform.position, Vector3(toMoveToAttack.transform.position.x, toMoveToAttack.transform.position.y, toMoveToAttack.transform.position.z) , moveSpeed * Time.deltaTime);
		}
	}
		
	if(Input.GetKeyDown("space"))	{
		Instantiate(bullet, transform.position, transform.rotation);
	}
}
	
function fireBullet()	{
	var bulletFire = Instantiate(bullet, theBarrel.transform.position, Quaternion.LookRotation(hit.transform.position - transform.position));
}

Ah, I see what the problem is. I suppose due to how Unity optimizes itself, Instantiate() and Destroy() is only guaranteed to be completed after the frame has finished executing. Since you’re doing a FindObject in the same frame, Unity will return the old location of moveTarget. This is simple enough to fix with an else statement separating your mouse detection and your movement code

function Update () {
    if(Input.GetMouseButtonDown(0)) {
        ...
    } else if ( isMoving ){
        ...
    }
}

But from a programming perspective, you’re using an awful lot of firepower just to keep a location in memory. I know it’s intuitive to spawn an object in the world and refer back to it later, but since all your movement is in one script (or in fact, even if you need to refer to the location in multiple scripts on different objects), there’s no need to go into such lengths to do so. You can simply keep a Vector3 as a variable.

#pragma strict
 
var turnSpeed : float = 180;
var moveSpeed : float = 10;

private var targetLocation : Vector3 = Vector3.zero;
private var isMoving : boolean = false;

private var targetRotation : Quaternion = Quaternion.identity;

function Update () {
    //Mouse Location awarenesss. When Mouse-Left is pressed, then...
    if( Input.GetMouseButtonDown( 0 ) ) {
        var ray = Camera.main.ScreenPointToRay( Input.mousePosition );
        //Casts a ray, stores the location and object it transected in Hit
        var hit : RaycastHit;
        
        if( Physics.Raycast( ray, hit, 1000) ){
        
            //If it is something else or Terrain...
            if( hit.transform.tag == "Terrain" ){
            
                targetLocation = hit.point + Vector3.up;
                targetRotation = Quaternion.LookRotation( targetLocation - transform.position );

                isMoving = true;
            }
        }
    }
    
    if( isMoving )    {
    	//If we're here
        if( Vector3.Distance(transform.position, targetLocation ) <= 1){
            isMoving = false;
            print("I'm here.");
        }
        //Otherwise, Move
        else {
        	transform.rotation = Quaternion.RotateTowards( transform.rotation, targetRotation, turnSpeed * Time.deltaTime );
            transform.position = Vector3.MoveTowards( transform.position, targetLocation, moveSpeed * Time.deltaTime );
    	}
    }
}

NOTE: hit.point gives you a Vector3, and Vector3.up is equivalent to (0, 1, 0), and yes you can add vectors like that ( this is the same as doing new Vector3( hit.point.x, hit.point.y + 1, hit.point.z) )

NOTE2: targetLocation - transform.position is not a cool way to find a direction you want your character to face. I have no way of knowing how tall your character is, but if it’s any more than 2 units tall this will make the character look down.

NOTE3: For the above code rotation will stop when you reach the target location, even if rotation isn’t finished yet. You’ll probably have to figure out for yourself if you want to keep rotating afterwards or not.