Creating an RTS in Unity: Part XIV

Update: This can now also be found at stormtek.geek.nz/rts_tutorial/part14.php, where the entire tutorial is now being hosted.

At long last it is time for a new post! I apologize for the delay. It has taken a lot longer to get this post together than I had initially planned. I knew it was going to be a while, but I did not expect that to stretch into several months. For that I am truly sorry.

This time round we will cover combat, the fun part of a game where you get to destroy things! We will only initiate this from UserInput for now, although our WorldObjects will be set up in such a way that they could easily be told to start attacks automatically through triggers of some sort. An important part of this will also be determining what happens to a WorldObject as it loses hitpoints.

Attack Cursor

Let’s start by detecting whether the Player can start an attack at the moment. We will indicate this to the user by changing the mouse cursor to an attack cursor. We need to start by extending SetHoverState() in WorldObject.cs. Replace the existing code with the code below

public virtual void SetHoverState(GameObject hoverObject) {
	//only handle input if owned by a human player and currently selected
	if(player && player.human && currentlySelected) {
		//something other than the ground is being hovered over
		if(hoverObject.name != "Ground") {
			Player owner = hoverObject.transform.root.GetComponent< Player >();
			Unit unit = hoverObject.transform.parent.GetComponent< Unit >();
			Building building = hoverObject.transform.parent.GetComponent< Building >();
			if(owner) { //the object is owned by a player
				if(owner.username == player.username) player.hud.SetCursorState(CursorState.Select);
				else if(CanAttack()) player.hud.SetCursorState(CursorState.Attack);
				else player.hud.SetCursorState(CursorState.Select);
			} else if(unit || building && CanAttack()) player.hud.SetCursorState(CursorState.Attack);
			else player.hud.SetCursorState(CursorState.Select);
		}
	}
}

and add the method CanAttack() which we will use to determine whether a specific WorldObject is able to attack things or not.

public virtual bool CanAttack() {
	//default behaviour needs to be overidden by children
	return false;
}

Notice that we have declared the method virtual to allow a class which extends WorldObject to override the default behaviour if necessary. By default a WorldObject will not be able to attack.

The logic being used for setting the hover state is as follows.

  • By default we want to show the selection cursor
  • If the WorldObject being hovered over is a Unit or a Building that is a) not owned by a Player or b) owned by another Player and the WorldObject that is being controlled can attack then show the attack cursor

If we wish to implement teams at some point then this code will need updating to make sure that the other Player is not on the same team as the Player that controls the selected WorldObject, rather than just checking that they are a different Player. There is also an assumption here that Players are not allowed to have the same name, since that is what we are using to differentiate between them.

For now we have just the one WorldObject that can attack things – our Tank. To allow it to be able to attack things add the following code to Tank.cs.

public override bool CanAttack() {
	return true;
}

We actually defined drawing of the attack cursor a number of posts back when we first added in custom cursors, so this code is enough to handle the changing of the cursor. If your objects still match mine then you should have a Building that is not attached to the Player. If you run your game now, select your Tank, and then hover your cursor over this Building you should be able to see the cursor change correctly.

Extra Player

To make things a little easier to test, though, we should add in another Player and give them some Units and Buildings. I think we have also reached the stage now where we can actually create a Player prefab too.

Before we create the prefab we should strip out everything that we do not want a brand new Player to have. This means we want to delete all Units and Buildings currently attached to our Player. (Before you do this make sure that each of them has already been made into a prefab, complete with BuildImages) You should be left with a Player object that contains a Buildings object and a Units object (both empty), an HUD object, and a RallyPoint object. Both the RallyPoint and the HUD should also be prefabs, so that we can make changes to the prefab and know that those changes will be applied to all Players that have an instance of those. Once your Player looks like this, turn it into a prefab (stored in the Player folder).

Now that we have a Player prefab we can add another instance of Player to the map. Set the name for one Player to Player1 and the name for the other Player to Player2. This makes sure that the two Players will be different if we compare them. Make Player1 a Human player (so that we can control that Player) and Player2 a Computer player (so that we have no control). With two Players in place add some Units and Buildings to each of them. We can do this by dragging new Building prefabs onto the Buildings part of our Player object and new Unit prefabs onto the Units part our our Player object. I have given each player a Refinery and a WarFactory, along with a Harvester, a Worker, and 2 Tanks. (Make sure that you spread these around the map a bit too. This is also a good time to make sure that the Position and Rotation for each prefab is (0, 0, 0))

If you run your game now you will see a collection of Units and Buildings scattered around. Unfortunately, we have no easy way to identify who owns what. We are only able to control the Units and Buildings of Player1, but the only way to tell what those are at the moment is to click on something and see if it responds. This is less than ideal.

Team Colour

To help an actual player to determine what is theirs we are going to introduce the ability to colour part of a WorldObject with a team colour. We will leave it up to a specific object to determine what this will be. Let us start by adding

if(player) SetTeamColor();

to the end of the Start() method in WorldObject.cs and then defining the method SetTeamColor().

protected void SetTeamColor() {
	TeamColor[] teamColors = GetComponentsInChildren< TeamColor >();
	foreach(TeamColor teamColor in teamColors) teamColor.renderer.material.color = player.teamColor;
}

This simply finds all the child objects of the WorldObject with the script TeamColor.cs attached and changes their colour to the team colour for the Player. Of course, we need to create this script before we can use this code. Create a new CSharp script in the WorldObject folder, name it TeamColor.cs, and set it’s contents as follows.

using UnityEngine;

public class TeamColor : MonoBehaviour {
	//wrapper class to allow a user-defined
	//part of a model to receive a team color
	//in the material of that part
}

Next we need to assign a team colour to a Player. Add

public Color teamColor;

to the top of Player.cs. This allows us to now set a team colour for each Player inside Unity. I have made Player1 to be a light blue (R: 100, G: 200, B: 250) and Player2 to be a medium red (R: 235, G: 20, B: 20).

Finally we need to define the parts of each WorldObect that we want to be the team colour. To do this is as easy as attaching the script TeamColor.cs to the desired object. The thing is, we want to perform this attachment on the prefab, not on an object already assigned to a Player. This is because we want to be able to guarantee that we do not have to perform this action every time we add a new object to a Player. This means that we need to delete all of the objects we just added to the Players, update the prefabs (which will require adding a copy to the map, deleting the prefab, making the changes, and then recreating the prefab), and then add the objects to the Players again. I will leave it up to you to decide which parts should be the team colour, though feel free to have a look at my version (found on github). Now when you run your game it should be easy to see which Player controls which Units and Buildings.

Note: If we delete a prefab that is referenced in GameObjectList we will need to remove the entry for it there. It is probably worthwhile at this stage checking to see that GameObjectList is up-to-date, since we are using it as the storage container for all objects we wish to create in our world.

If you have been paying close attention to how things ran prior to this post you will notice that we have introduced a subtle bug. Our Units and Buildings now all have a part of them designated to be a team colour, which is great for identifying which Player it belongs to. Unfortunately, when we create a new Building it is transparent during the build process. Once construction is completed it reverts to the colours it had, but it forgets the team colour. We need to fix this, since construction is going to be the primary way that our Players gain new Buildings. It actually turns out that this is really easy to fix. All we need to do is add a call to SetTeamColor() to the end of Construct() in Building.cs, resulting in the following code.

public void Construct(int amount) {
	hitPoints += amount;
	if(hitPoints >= maxHitPoints) {
		hitPoints = maxHitPoints;
		needsBuilding = false;
		RestoreMaterials();
		SetTeamColor();
	}
}

Initiate Attack

Now that we can distinguish between Players and we can indicate to a Player that an attack can be launched it is time to actually start that attack. We start by updating the code in MouseClick() found in WorldObject.cs.

public virtual void MouseClick(GameObject hitObject, Vector3 hitPoint, Player controller) {
	//only handle input if currently selected
	if(currentlySelected && hitObject && hitObject.name != "Ground") {
		WorldObject worldObject = hitObject.transform.parent.GetComponent< WorldObject >();
		//clicked on another selectable object
		if(worldObject) {
			Resource resource = hitObject.transform.parent.GetComponent< Resource >();
			if(resource && resource.isEmpty()) return;
			Player owner = hitObject.transform.root.GetComponent< Player >();
			if(owner) { //the object is controlled by a player
				if(player && player.human) { //this object is controlled by a human player
					//start attack if object is not owned by the same player and this object can attack, else select
					if(player.username != owner.username && CanAttack()) BeginAttack(worldObject);
					else ChangeSelection(worldObject, controller);
				} else ChangeSelection(worldObject, controller);
			} else ChangeSelection(worldObject, controller);
		}
	}
}

There are a couple of things to note here. First, we only allow a Player to attack objects that belong to another Player. Secondly, a Player cannot attack objects that belong to them. Finally, an attack is only launched if the object can launch attacks. Also, the default behaviour is to change the selected object. With this code in place we now need to define the method BeginAttack(), which is where all the work of starting an attack will actually happen.

protected virtual void BeginAttack(WorldObject target) {
	this.target = target;
	if(TargetInRange()) {
		attacking = true;
		PerformAttack();
	} else AdjustPosition();
}

The logic here is to only attack if the target is close enough. If not the WorldObject needs to move closer to the target. But before we do that we want to save the target that is to be attacked. This means that we need to add

protected WorldObject target = null;

to the top of WorldObject.cs so that we can store this reference. Next we need to add

protected bool attacking = false;

to the top of WorldObject.cs, which we will use to indicate whether this object is attacking something or not. Finally, we need to add the three methods which handle the working out of the logic for this method. We will start with working out whether the target is in range or not.

private bool TargetInRange() {
	Vector3 targetLocation = target.transform.position;
	Vector3 direction = targetLocation - transform.position;
	if(direction.sqrMagnitude < weaponRange * weaponRange) {
		return true;
	}
	return false;
}

The target is in range if the distance to the target is less than the range of the weapon for the WorldObject. For us to know what this range is we need to add

public float weaponRange = 10.0f;

to WorldObject. By specifying a value here we can guarantee that each WorldObject has a default range (I have gone through and added defaults to most values now so that a new type of WorldObject will work without weird errors). Since it is public it also means that we can play around with the range inside Unity.

While looking into how best to work out the distance to the target I found docs.unity3d.com/Documentation/Manual/DirectionDistanceFromOneObjectToAnother.html which notes that for a simple distance calculation we can use the square of the magnitude, since that takes less work to calculate. This is what makes a seemingly simple check look a little more complicated, but every little bit of time saved in processing is worth it in the long run.

Now that we know whether a target is in range or not, let’s specify how to adjust the position of our WorldObject.

private void AdjustPosition() {
	Unit self = this as Unit;
	if(self) {
		movingIntoPosition = true;
		Vector3 attackPosition = FindNearestAttackPosition();
		self.StartMove(attackPosition);
		attacking = true;
	} else attacking = false;
}

At the moment the only object that can move closer to a target is a Unit. If the WorldObject is not a Unit then we cancel the attack (since it is not possible anyway). Otherwise we want to find the closest point between the WorldObject and the target which is in range and then start moving towards that. This method that performs this calculation needs to be added in now.

private Vector3 FindNearestAttackPosition() {
	Vector3 targetLocation = target.transform.position;
	Vector3 direction = targetLocation - transform.position;
	float targetDistance = direction.magnitude;
	float distanceToTravel = targetDistance - (0.9f * weaponRange);
	return Vector3.Lerp(transform.position, targetLocation, distanceToTravel / targetDistance);
}

We know where the WorldObject is, and we know where the target is. From this we can calculate the vector between the two objects. This vector gives us the distance to the target, from which we want to subtract the weapon range. We will actually subtract 90% of the weapon range so that once the Unit is in position it will not need to move immediately if the target should move. This gives us the distance that we need to travel along the vector between the WorldObject and the target. We then use all of these details to execute the Unity linear interpolation method on a Vector3 to calculate the position in the world that is the correct distance along the vector. This is the position that needs to be returned as the nearest attack position.

We also need to add

protected movingIntoPosition = false;

to the top of WorldObject.cs to help with us working out what the current state of our WorldObject is. To make sure that a Unit updates it’s state upon completion of movement we also need to add

movingIntoPosition = false;

into MakeMove() in Unit.cs when the Unit has reached it’s destination. The resulting method is as follows.

private void MakeMove() {
	transform.position = Vector3.MoveTowards(transform.position, destination, Time.deltaTime * moveSpeed);
	if(transform.position == destination) {
		moving = false;
		movingIntoPosition = false;
	}
	CalculateBounds();
}

Now that we know how to get into range it is time to define how to perform an attack.

private void PerformAttack() {
	if(!target) {
		attacking = false;
		return;
	}
	if(!TargetInRange()) AdjustPosition();
	else if(!TargetInFrontOfWeapon()) AimAtTarget();
	else if(ReadyToFire()) UseWeapon();
}

This method is going to be called regularly while the WorldObject is attacking, so we will add in some extra checks that are useful at the same time. The first one is to make sure that the target has not been destroyed already (by this WorldObject or some other WorldObject). If it has we need to stop the attack at once. Again we check to see whether the target is in position, since it might have managed to move away from the WorldObject. Next we check to see whether the target is in front of the weapon the WorldObject has and adjust position accordingly. Finally we check to see whether the weapon is ready to fire (since we want to enforce a regular rate of fire) and only actually use the weapon if the WorldObject is ready to.

Again we need to add some more code to our WorldObject. Let’s start with aiming the weapon.

private bool TargetInFrontOfWeapon() {
	Vector3 targetLocation = target.transform.position;
	Vector3 direction = targetLocation - transform.position;
	if(direction.normalized == transform.forward.normalized) return true;
	else return false;
}

The check is quite straightforward. If the vector between the WorldObject and the target equals the forward vector for the WorldObject then the target is in front of the WorldObject. At this stage we are assuming that the weapon for a WorldObject always points out the front of the WorldObject. If you wish to change this for a specific object then that object will need to override this method (in which case this will need to be a protected virtual method, rather than a private method).

protected virtual void AimAtTarget() {
	aiming = true;
	//this behaviour needs to be specified by a specific object
}

We will leave most of the behaviour for aiming at a target up to a specific object. All we want to do is to set a state variable so that we can work out whether the WorldObject is aiming their weapon or not. This needs to be added to the top of WorldObject now.

protected bool aiming = false;

The one WorldObject we have at the moment which can attack at the moment is our Tank. To make sure that this turns towards a target properly we need to add some code to Tank.cs.

protected override void AimAtTarget () {
	base.AimAtTarget();
	aimRotation = Quaternion.LookRotation (target.transform.position - transform.position);
}

Here we are working out what rotation is needed to turn the Tank towards a target. To store this we need to add

private Quaternion aimRotation;

to the top of Tank.cs. To actually perform the rotation we need to adjust the Update() method for our Tank.

protected override void Update () {
	base.Update();
	if(aiming) {
		transform.rotation = Quaternion.RotateTowards(transform.rotation, aimRotation, weaponAimSpeed);
		CalculateBounds();
		//sometimes it gets stuck exactly 180 degrees out in the calculation and does nothing, this check fixes that
		Quaternion inverseAimRotation = new Quaternion(-aimRotation.x, -aimRotation.y, -aimRotation.z, -aimRotation.w);
		if(transform.rotation == aimRotation || transform.rotation == inverseAimRotation) {
			aiming = false;
		}
	}
}

Now let’s find out whether the weapon of the WorldObject is ready to fire or not.

private bool ReadyToFire() {
	if(currentWeaponChargeTime >= weaponRechargeTime) return true;
	return false;
}

To track the weapon readiness we need to add

public float weaponRechargeTime = 1.0f;
private float currentWeaponChargeTime;

to the top of WorldObject.cs. This allows us to vary the time taken to recharge the weapon within Unity. The current charge time will be modified in the Update() method (which we will get to in just a minute). If the current charge time is greater than the required time then the weapon is ready to fire.

Once the weapon is ready to fire, it is time to actually use it.

protected virtual void UseWeapon() {
	currentWeaponChargeTime = 0.0f;
	//this behaviour needs to be specified by a specific object
}

Again we will leave the actual implementation of using a weapon up to a specific object. What we do want to do is to reset the current charge time for the weapon back to 0 each time the weapon is used. This guarantees that the weapon needs to recharge after use.

Before we get to implementing the firing of the weapon for our Tank, we need to make sure the Update() method for our WorldObject is adjusted so that we can use the weapon regularly while the WorldObject is attacking.

protected virtual void Update () {
	currentWeaponChargeTime += Time.deltaTime;
	if(attacking && !movingIntoPosition && !aiming) PerformAttack();
}

Here we are increasing the current charge time for the weapon each update. It does not matter if this gets too high, since as long as it is greater than the weapon charge time we are able to fire the weapon. We also want to perform an attack if the WorldObject is attacking but is not also moving into position or aiming at the moment. If you run this now you should be able to see your attack vehicles move into position, although they will not yet be using any weapons.

Use Tank Weapon

Before we finish up this time it would nice to be able to destroy something. To do this, let’s provide an implementation of UseWeapon() for our Tank.

protected override void UseWeapon () {
	base.UseWeapon();
	Vector3 spawnPoint = transform.position;
	spawnPoint.x += (2.1f * transform.forward.x);
	spawnPoint.y += 1.4f;
	spawnPoint.z += (2.1f * transform.forward.z);
	GameObject gameObject = (GameObject)Instantiate(ResourceManager.GetWorldObject("TankProjectile"), spawnPoint, transform.rotation);
	Projectile projectile = gameObject.GetComponentInChildren< Projectile >();
	projectile.SetRange(0.9f * weaponRange);
	projectile.SetTarget(target);
}

Our Tank is going to make use of a very simple projectile weapon. This needs to be spawned at the barrel of the gun (which is now pointing at the target) and sent off to hit the target. We need to make use of the forward vector for the Tank to help us find the position where the Projectile needs to be spawned. To find this I positioned a Tank at (0, 0, 0) and then positioned a Projectile where I wanted it to appear. Since everything still has a fixed y position (on the plane representing our ‘ground’) that remains the same. The x and z positions need to be multiplied by the x and z positions of the forward vector (not the location vector) of our Tank. This gives us the position in the world where we want to create a new Projectile. We are then using the Unity method Instantiate to create the object we want, getting the Projectile object attached to that, and setting the target and range appropriately. The range is set to a little less than the actual weapon range since it was appearing out the far side of the target for some weird reason.

Now, I can hear you thinking “Wait a minute, we don’t have a Projectile yet”, and this is true. But that is something that is easily fixed. First, let us start by creating the necessary scripts. Inside the WorldObject folder create a new CSharp script called Projectile.cs and set it’s code as follows.

using UnityEngine;
using System.Collections;

public class Projectile : MonoBehaviour {

	public float velocity = 1;
	public int damage = 1;

	private float range = 1;
	private WorldObject target;

	void Update () {
		if(HitSomething()) {
			InflictDamage();
			Destroy(gameObject);
		}
		if(range>0) {
			float positionChange = Time.deltaTime * velocity;
			range -= positionChange;
			transform.position += (positionChange * transform.forward);
		} else {
			Destroy(gameObject);
		}
	}

	public void SetRange(float range) {
		this.range = range;
	}

	public void SetTarget(WorldObject target) {
		this.target = target;
	}

	private bool HitSomething() {
		if(target && target.GetSelectionBounds().Contains(transform.position)) return true;
		return false;
	}

	private void InflictDamage() {
		if(target) target.TakeDamage(damage);
	}
}

We assign a velocity and damage variable which can be tweaked inside Unity. These determine how fast the Projectile moves and how much damage it deals when it hits something. We also have a range and a target which can only be set by another object (normally the one that creates the Projectile in the first place). This makes sense, since the Projectile itself does not know how far it can travel, or what it is meant to hit.

The logic of what the Projectile can do is all handled inside the Update() method. First off, if the Projectile hit something we need to inflict damage on that object and then destroy the Projectile. It would be at this point that we would initiate an explosion sequence if we so desired, but I will leave that up to you. Rather than using another variable to keep track of how far the Projectile has traveled we will reduce range as it moves. This means that the Projectile should be moved forward if it still has range left. We need to work out how far to move the Projectile, subtract that value from the range, and then adjust the position of the Projectile. If there is no range left then the Projectile needs to be destroyed (this makes sure that we do not clutter our world with Projectiles which have missed their targets).

To check whether the Projectile has hit something we are going to keep things very simple for now. Ideally we would be checking for a collision with any object in our world. But for now we will just check to see if the Projectile has hit the specified target. This does mean that we will have glitches like being able to shoot through other objects to hit our target. But for now, this will be sufficient. (If we were to detect that the Projectile had hit something else here we could change target to be that object, which would mean that the rest of the code will still work). Dealing damage is as simple as telling the target object how much damage to lose. This does mean, however, that we need to add a new method to WorldObject.cs to handle this.

public void TakeDamage(int damage) {
	hitPoints -= damage;
	if(hitPoints<=0) Destroy(gameObject);
}

Once a WorldObject has no hitPoints left we need to destroy it. Once again, a fancy death sequence would be added here if we wanted it. This is another thing that you can play around with in your own time.

Ok, now that we have code to handle a Projectile we need an object to attach that to. Create a new empty object and call it TankProjectile. Add to this a new capsule with the following properties: Position = (0, 0, 0), Rotation = (0, 90, 90), Scale = (0.4, 0.3, 0.4). I added the Metal material that we created a while back to the capsule so that it looks more like a Tank shell.

To allow us to potentially extend things later on I created a new CSharp script called TankProjectile.cs inside the Tank folder which simply extends Projectile.cs (for now).

using UnityEngine;
using System.Collections;

public class TankProjectile : Projectile {}

Attach this script to the TankProjectile object that you created. Now drag this object down into the Tank folder to create a TankProjectile prefab object that we can instantiate. Next add the prefab to the WorldObjects list in our GameObjectList to allow the code we have above to actually find this prefab when the Tank goes to instantiate a new TankProjectile. I have set the damage for this prefab to 10 and the velocity to 30. This gives us a moderately fast projectile that does light damage. You should now be able to run your game and take one of your Tanks out to destroy some of the ‘enemy’.

And that wraps it up for this time. We now have the ability to tell our Buildings / Units to initiate attack against objects belonging to opposing players. As usual the code can be found at github under the commit for Part 14. I hope that the next part comes quicker than this one did, but unfortunately I cannot make any promises as to exactly when that will be. Until next time, have fun mucking around with what has been established so far!

Advertisements

37 thoughts on “Creating an RTS in Unity: Part XIV

  1. Joel says:

    Thanks for this tutorial. You forgot to include where you set back to false: movingIntoPosition = false (in Unit.cs -> MakeMove()).This causing the tank to not attack properly. I thought I would mention it so others don’t get stuck.

    • Thanks for pointing that out. I do have it in place in the code on github, but I obviously forgot to point it out when writing up the post. I have added a mention of that into the post itself now, so others doing it should not get stuck in the same place any more.

  2. Eldoud says:

    For the colours of the objects, Unity says :
    MissingComponentException: There is no ‘Renderer’ attached to the “Tank” game object, but a script is trying to access it.

    So I added a Mesh Renderer to the Objects and modified SetTeamColor() because when you change the colour of the mesh renderer of the main object nothing happen, you have to change all the renderers of the objects which belong to the main object :

    SetTeamColor() {
    TeamColor[] teamColors = GetComponentsInChildren ();
    foreach (TeamColor teamColor in teamColors) {
    foreach (Renderer rend in teamColor.GetComponentsInChildren()) {
    rend.material.color = player.teamColor;
    }
    }
    }

    Where is my problem please ? (I mean why don’t you have this problem)

  3. anon says:

    question:

    How can we be sure this line gets the correct player?

    Player owner = hoverObject.transform.root.GetComponent();

    Doesn’t transform.root get the root folder of the hierarchy, where all players will be located?

      • It gets the top object not the root folder. This could be an empty object, it could be the plane we are using for the ground, it could be the camera (although there is no way to select this in the game). In the case of checking for the Player it would be the empty object we created for a Player which has Player.cs attached to it.

  4. anon says:

    “This means that we need to delete all of the objects we just added to the Players, update the prefabs (which will require adding a copy to the map, deleting the prefab, making the changes, and then recreating the prefab), and then add the objects to the Players again.”

    I think you can click the small arrows in the prefabs and edit the parts directly in the prefab. Would make it easier to update prefabs while not having to recreate them in your scene.

      • anon says:

        Aight, found nothing wrong this time, except that you need to set attacking=false; when you move after starting to attack something. You probably fix this later, though.

        Also, I think the enemy hud or rally point triggers setting cursor to attack mode, as when I mouse over 0.0.0~ the cursor changes.

  5. anon says:

    Noticed a bug: When I create a new building, the rally point seems to be placed at a completely random spot. But if I drag a prefab in and the building is created before the game itself runs (aka. it wasn’t constructed), the rally point shows up in the correct spot.

    • anon says:

      Solution found:

      move this:

      float spawnX = selectionBounds.center.x + transform.forward.x * selectionBounds.extents.x + transform.forward.x * 10;
      float spawnZ = selectionBounds.center.z + transform.forward.z + selectionBounds.extents.z + transform.forward.z * 10;
      spawnPoint = new Vector3(spawnX, 0.0f, spawnZ);
      rallyPoint = spawnPoint;

      in Building.Awake() to it’s own Init() function. Call Init() when you place the building. I think what happens is that the spawn stuff gets set wrong when you create the “ghost building” for placement.

      Noticed another bug, too. When you click to place another building when you already have a ghost building over your cursor, you get a new one, but the old one stays on the screen. Probably just have to delete the temp building when you select a new one.

        • anon says:

          Actually found a solution real quick:

          Add:
          tempBuilding = null;

          At the point where you want to no longer delete the building when you select to build a new one. This should be where you choose to put RestoreMaterial. In my case this is in protoharvesters update() when it has arrived at the site to construct the building. If you’re doing the same, that means you need to create a ClearTempBuilding function in Player since it’s private. It’s probably good practice to clear tempCreator here too, but it seems to work without doing it.

          And then add:
          if (tempBuilding)
          Destroy(tempBuilding.gameObject);

          At the beginning of CreateBuilding(…).

          • At some point I am thinking of doing a ‘Random tweaks and fixes’ post, so if I do I would include things like this in that. Thanks for your poking around and commenting on things.

  6. anon says:

    I’m currently working on implementing melee functionality for units. Any thoughts? I’m thinking of adding the extents of the target selectionbox to range as a “base range”. Then in Start set if range<extents of units selectionbox, increase to extents. Then range=0 should be melee. I'll then add a test of if range=0, don't spawn projectile and handle stuff differently.

    Sounds reasonable?

    • I haven’t really thought melee combat through yet (in part because it is the more difficult scenario to handle). But you will want a range of not very much – quite possibly 0. And if the idea is to have the weapon being used (sword, axe, etc) as part of the unit (which makes sense in terms of control) then it will form part of the selectionBounds for that Unit.

      • anon says:

        Range of flat 0 doesn’t work because that literally puts the unit inside it’s target (or bouncing off it after implementing collision, I guess). What I’m aiming for is something like wc3 where the two units selectionboxes pretty much border each other, and then the unit attacks (and deals damage). That’s why I thought about that math I mentioned.

        I’m using your tutorial as a groundwork for my Bachelor thesis (will source, obviously) ^.^ I’m going to expand on it a lot, of ‘course, but for now it has accelerated my learning curve into Unity, so thanks 🙂

        • anon says:

          Hmn, when I attach a collider i’m not able to select units anymore. I’m assuming the findhitobject isn’t working properly, but I don’t see why.

          if (Physics.Raycast(ray, out hit))
          return hit.transform.gameObject;

          Since the collisionbox is attached to the tank object, it should find the connected gameobject when it hits the collisionbox it has attached. But for some reason it doesn’t. Hmn.

          • Yea, some weird things seem to happen with colliders. And they don’t seem to handle detections of collisions (i.e. ‘I collided with something’) very well either, which seems to defeat the point a bit. It has been quite a while since I have played with that of course.

        • I’m glad that I can help out in your learning 🙂 What is your Bachelor thesis on? I am also using what I have learned here to help out a guy doing a PHD using Unity as the platform to build up an environment he will be using for experiments. It is pretty cool the sorts of things that can be done with this – and with the free version too.

          • anon says:

            Well I’m making an RTS-game, obviously, and the focus point is supposed to be AI and Pathfinding. But I need to get the core engine running before I can play with that.

            You can use isTrigger to handle collision yourself, but it still stops the raycast from working. No idea why.

  7. anon says:

    New thread due to comment nesting:

    Found a neat thing. Edit->Project Settings->Physics. Tick Raycast ignore Triggers. Then you can use the trigger for collisions manually, but the raycast will ignore it. Seems a bit weird to have to do it this way (the collider is connected to the object, so it should just.. work), but it makes it work at least.

    • anon says:

      Alternative solution: Add empty gameobject to “Ignore raycast” layer. Your raycast will still hit the sub-objects the object is made up of (boxes, cylinders, etc.) and work. Then you don’t have to use the isTrigger option if you wanna use Unity’s default collision.

    • I will have to investigate this further at some point. At the moment I am still playing around with higher level ideas though. While it would be great to play a completed game, I am more interested in getting the big picture working than I am in getting all of the small details working (though they are just as important).

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