Footy folley – adding CPU & Finalizing prototype

 estimated time : 14mn
ingredients needed : Unity, Playmaker
(this recipe is part of the footvolley game menu )

 

  • Duplicate player to create an opponent
  • Creating new states and vars for CPU
  • Implement CPU Controller Script
  • Changing FSM logic

 

00:00 Creating opponent

Duplicate the Player1 gameobject, rename it “CPU”, then place it on the other side of the game field.
It’s a good start, but our CPU is facing right, let’s it face left by changing it X scale from 1 to -1.

Then delete the player Controller script, we’ll later add the CPU Controller Script instead.

 

00:38 Renaming Vars

Now rename some vars; as originally all was created with values for player1 in mind, it’s time to duplicate needed values for the CPU.

We took all states one by one and check what to change and/or remove.

In Setup state:

  • Change “origin_colliderScale” to CPUorigin_coliderscale”.

In Idle state:

  • Remove the getKey down action, we don’t need to wait for CPU inputs, we will tell it what to do by script.
  • Remove the “can Move =true” action.
  • “Set default collider scale action”, change for the CPUorigin_colliderScale, and create a new var : CPUcurr_scale.
  • “Set scale” actions, change the values for the new “CPUcurr_scale” vars.
  • “Set curr power = 0”, create the new var for the CPU : “CPUcurr_power”.
  • Remove the “is kick released action”.

In isSliding state :

  • Remove the “if” states, we don’t need to know if the player1’s sliding or not. Moreover the slide action is actually pending for the CPU, we’ll decide later if we keep it or not.

In Sliding state :

  • Remove the set boolean action on canMove. unneeded for CPU.
    same thing for isKickReleased.Those booleans were created to prevent the player to do things in the middle of actions, we cannot stop slide by just moving elsewhere in the middle of it, it’ll be too easy.
    But the CPU is a more compliant actor, if we tell it not to move until a slide is over, he’ll not move.
    main rule : Players are stubborn and try to break laws, CPUs aren’t.
  • Change the translate vars p1X & p1Z to CPUX & CPUZ.

In Release Kick state :

  • Remove isKickRelease = true.
  • Change curr_power to CPUcurr_power.
  • Change curr_scale to CPUcurr_scale.
  • Create a new CPUAim_pos to replace Aim_pos.

 

 

04:47 Changing CPU state to follow ball

In order to get the CPU doing things when we want to, we’ll give to him various states.

Wandering

When the CPU does nothing, he’s just wait for the player to kick the ball to react.

Following

When the player kicked the ball, the CPU will go right where the player aimed.

CPUready

Once the player reached the place where the ball will land, he stands there and charge for power.

Aiming

Once the ball reach the CPU, the cpu will aim at a random point in the opposite side, and kick the ball; before entering in wandering mode again.

These states are detailed in the CPU Controller Script, into the update() method. Like this on every frame the CPU will know what to do, if he’s on “following” state, each frame he will move towards the player aim for example.

 

First thing to do in Idle state, it’s to set the CPU’s state to “wandering”, to match the FSM state with the CPU script reaction. We use the Set  FSM string action.

We create a new string variable called “CPUState”, and we input it as the Variable name, and we set it to the “wandering” value.
As described in the above recipe, it’s really important to input yourself the “CPUState” as a variable name and not to point to “CPUState” value from the list; as it would means that you try to set the “wandering” string to a value pointed by CPUState.

Now let’s create the Follow ball state, to set the “following” CPUstate. Create a new “Follow ball” state in the CPU game object.

Then copy/paste the previous set FSM string action into it, and change it to set the CPUState var as “following”.

We’ll switch to this state when the player kicks the ball.
remember that when the player kicks the ball, it fires the “PlayerKick” global event?

So add a global transition event to “Follow ball” with the event “PlayerKick”

But one important thing to change to make things done; in the P1 collider, we stipulate to sent this event only to the ball.
But now our CPU needs to know when the player kick the ball too; so we change to fire this event globally, by changing it to « broadcast all » instead of a single gameObject.
We test a little, on the FSM we clearly see that it’s okay in the CPU FSM. So we can move on.

 

06:53 Adding CPU reaction time

The CPU shouldn’t react as soon as the player kicks the ball.
It’s unfair compared to our slow brain, so to compensate a little, we’ll add a random reaction time.

Create a new cpu state « reaction time », add a random wait action/ change our global transition when the player release kick here, then once the random wait is over, let’s make a transition to the “follow ball” state.

CPU Controller Script

Time to see the full CPU controller script to see all the various sates and methods  :

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using HutongGames.PlayMaker;
 
public class CPUController : MonoBehaviour {
 
//public declarations
public PlayMakerFSM MyFsm;
public int CPUSpeed;
//private declarations
private bool isReducingCollider=false;
private FsmString CPUstate;
private FsmFloat CPUcurr_power;
private Animator anim;
private Vector3 moveToward,moveDirection,destination;
private GameManager gameManager;
private bool reachDestination = false;
private float startCount,aimWidth,powerStored;
 
 
void Awake(){
//retrieve the FSM component from the CPU gameObject
	MyFsm = this.GetComponent();
}
 
// Use this for initialization
void Start () {
//retrieve the game manager that holds main stored values
	gameManager = GameManager.Instance();
//retrieve animator component
	anim = GetComponentInChildren<Animator>();
//retrieve FSM values
	CPUstate = MyFsm.FsmVariables.FindFsmString("CPUstate");
	CPUcurr_power = MyFsm.FsmVariables.FindFsmFloat("CPUcurr_power");
}
 
	// Update is called once per frame
	void Update () {
//here we use a switch statement to control the various
//CPU behaviors based on the CPUState
		switch (CPUstate.Value){
//cpu wanders : do nothing
			case "wandering":
			break;
//cpu follows the ball
			case "following":
			FollowBall();
			break;
//cpu aim and kicks back the ball
			case "aiming":
			KickBall();
			break;
//cpu stands and increase power
			case "CPUready":
			AddPower();
			break;
//default value as wandering, because s**** happens
			default:
			CPUstate.Value = "wandering";
			break;
		}	
	}
 
//these are called from FSM states with send message action
//used to display shooting animations
void start_shootAnim(){
	anim.SetBool("player_shoot",true);
}
void stop_shootAnim(){
	anim.SetBool("player_shoot",false);
}
 
 
void FollowBall(){
//used to enters the walking anim
	anim.SetFloat ("player_speed", 1);
//inits to know which vector the CPU should move towards to
	moveToward = gameManager.ballAim;
	Vector3 currentPosition = transform.position;
	moveDirection = moveToward - currentPosition;
	moveDirection.Normalize ();
	Vector3 target = moveDirection * CPUSpeed + currentPosition;
	transform.position = Vector3.Lerp (currentPosition, target, Time.deltaTime);
//check if position is (nearly) reached
	if (!reachDestination) {
		if (System.Math.Round (moveToward.x, 1) == System.Math.Round (currentPosition.x, 1)) {
			if (System.Math.Round (moveToward.z, 1) == System.Math.Round (currentPosition.z, 1)) {
//once CPU reach the aim pos, set reachDestination to true,
//to exit this loop
				reachDestination = true;
				//timer for ballSpeed
				startCount = Time.time;
				anim.SetFloat ("player_speed", 0);
//change the CPU state
				CPUstate.Value = "aiming";
			}
		}
	}
}
 
	void KickBall(){
		//set random aim
		RandomAimPos();
 
//set CPU to CPUready state, to start saving power for the shoot
		 MyFsm.Fsm.Event("CPUready");
 
	}
 
	void RandomAimPos(){
//this is the resetAimPos() method from the playercontrollerscript
//with some little tweaks to make it works from the other side
		aimWidth = gameManager.aimWidth;
 
		//we retrieve the transform position of the field stored in our gameManager and add or remove the aim with to it.
		float xmin =(gameManager.terrainL.transform.position.x-(gameManager.terrainL.size.x/2))+aimWidth;
		float xmax= gameManager.terrainL.transform.position.x+(gameManager.terrainL.size.x/2)-aimWidth;
 
		float zmin =((gameManager.terrainL.size.z/2)*-1)+aimWidth;
		float zmax= (gameManager.terrainL.size.z/2)-aimWidth;
 
		//create a new vector with random clamped values
		Vector3 pos = new Vector3 (Random.Range (xmin, xmax), gameManager.terrainL.transform.position.y, Random.Range (zmin, zmax));
 
		//then we instantiate an aim prefab with killing time
		GameObject instance = Instantiate(Resources.Load("Player2Aim", typeof(GameObject))) as GameObject;
		instance.transform.position = pos;
		//set the ballAim new value
		gameManager.ballAim = pos;
 
 
}
 
void AddPower(){
	 CPUcurr_power.Value += 0.005f*Time.deltaTime;
	 gameManager.power = CPUcurr_power.Value;
}
 
 
 
}

07:45 Deleting CPU aim

Now that our CPU has moved at the right place (understand where the ball will lands), he needs to aim into the opposite side.
So we’ll add a visual clue to the player to see where he should go right away.
First delete the aim game object inherited from the previous Player1 duplicate : we’ll use another gameObject here.

When you delete the aim gameobject, you’ll see in the FSM multiple red marks, it’s where we made calls to this gameobject (to enable or disable it, mostly). Get rid of these calls. We can even delete the init Aim state, as we’re now coding the CPU aiming part directly in the CPUController script.

We created in the resources folder a copy of the aim gameObject, but with a killer script attached to it, set it to 1, and voilà : an aim that will appear where we want to and disappears 1 sec after; Perfect clue.

For now if we test, we see that once the CPU has moved to the correct place, it stays in aiming mode and so choose a random part of the field to aim, and attach an aim to it. In the video, As the code stops there, the CPU stays in the initAim mode loop forever, so we see it choosing multiple aim spots.
Anyway, the CPU reacts as we want for the moment, and that’s cool enough.

 

09:08 CPU ready event

Now that the CPU’s in place to intercept the ball and has choose an aim.
Let’s put it in ready mode.

Ready to be kicking the ball, so go to the isKickingState state, and clear it from useless actions. Well useful for a human player, but not for our CPU. See all the actions to delete in the video.

 

By the way, we didn’t have the needs of the aim object in the cpu anymore, so delete it.
Watch where it was used in the FSM and delete the actions that previously refers to it.

We set the “CPUReady” global transition to our “isKicking” CPU state, this state is called within the CPUControllerScript

We now need to create a global bool to track if the cpu’s in place when the ball touches the ground.
Our logic is as follows, and will be implemented into the soccer ball FSM :

  • If the ball touches the ground (after the player1 kicks) of the CPU field and the CPU is not ready, so we assume the CPU didn’t make it and the point is for the player? Excepted if the ball reach the OUT zone of the field, even if the cpu is on it, he will not sent it back (even on low IA, he’s not that stupid).
  • But if the CPU is ready so we launch the send back ball method.

So create a isCPUReady global boolean, set it to true with the SetBool value action .
Delete the “activate collider” action, as for the CPU we’ll know if we touch the ball depending if he reached the spot in time or not.

11:18 Changing Soccer ball FSM logic

Time to change the logic into the soccer ball FSM :

 

after player shoots the ball, if the ball hit the ground, is it IN or OUT?
OUT: point to CPU

IN: is the cpu ready?
yes: sent back the ball
no: point to player

To achieve this, create a new state in the soccer ball :  “is CPU ready”, we link the ” is ball grounded in” to our new “is CPU ready”.
Then we use a boolean test compare action , we check the value “isCPUReady”.

If the bool is false, it’ll sent the groundR transition, that’s linked to the player point state.

If it’s true we create a new transition event “CPUCatch” (for CPU catches the ball), that’s linked to a new state “CPU Catch ball”  (yeah it was really late when i wrote this, you know..).

This state use the sent event action, it fires the “CPUKick” event, but we tell to broadcast this event to all. Meaning not only the soccer ball will get the CPUKick event, telling it to check the logic of kicked 2 ; but our CPU game object will see it too.

So we add the global transition CPUKick in the release kick CPU’s FSM.
Telling the cpu to display the kick animation, and return right after to the idle state. and looping in the wandering state, waiting to see if the player is able to kick the ball back.

 

Hey, it works ?

Well, well, well…We go a little prototype on our own now, so what now is it over?
Of course not, it’s only the first step.

If you want  full game you need to polish, polish, polish and add new features one by one.
Here are some clues, think of it as homework :

  • You should randomly moves the CPU while in its wandering loop.
  • You must  add a service phase for the cpu too, using non erasable values to keep tracks on which got the point and which should do the service.
  • You can narrow the random cpu wait time between values depending of the AI level (the smartest the cpu is, the lower is the reaction time)
  • Within CPU AI : precision of sending ball in (need to choose between rand  value in big field  or in IN field)
  • The cpu should slide too !

 

 

 

 

Leave a Reply