Air hockey is probably one of the most popular games enjoyed by players of all ages during the golden age of arcades, and they are still found everywhere. With the advent of touchscreen mobile devices, developing an air-hockey game is a fun way to not only test physics engines, but also to develop intelligent rivals despite the apparently low complexity of the game.
This is a technique based on some of the algorithms that we learned in Chapter 1, Movement, such as Seek
, Arrive
, and Leave
, and the ray casting knowledge that is employed in several other recipes, such as path smoothing.
It is necessary for the paddle game object to be used by the agent to have the AgentBehaviour
, Seek
, and Leave
components attached, as it is used by the current algorithm. Also, it is important to tag the objects used as walls, that is, the ones containing the box colliders, as seen in the following figure:
Finally, it is important to create an enum
type for handling the rival's state:
public enum AHRState { ATTACK, DEFEND, IDLE }
This is a long class, so it is important to carefully follow these steps:
using UnityEngine; using System.Collections; public class AirHockeyRival : MonoBehaviour { // next steps }
public GameObject puck; public GameObject paddle; public string goalWallTag = "GoalWall"; public string sideWallTag = "SideWall"; [Range(1, 10)] public int maxHits;
float puckWidth; Renderer puckMesh; Rigidbody puckBody; AgentBehaviour agent; Seek seek; Leave leave; AHRState state; bool hasAttacked;
Awake
member function for setting up private classes, given the public ones:public void Awake() { puckMesh = puck.GetComponent<Renderer>(); puckBody = puck.GetComponent<Rigidbody>(); agent = paddle.GetComponent<AgentBehaviour>(); seek = paddle.GetComponent<Seek>(); leave = paddle.GetComponent<Leave>(); puckWidth = puckMesh.bounds.extents.z; state = AHRState.IDLE; hasAttacked = false; if (seek.target == null) seek.target = new GameObject(); if (leave.target == null) leave.target = new GameObject(); }
Update
member function. The following steps will define its body:public void Update() { // next steps }
switch (state) { case AHRState.ATTACK: Attack(); break; default: case AHRState.IDLE: agent.enabled = false; break; case AHRState.DEFEND: Defend(); break; }
AttackReset();
public void SetState(AHRState newState) { state = newState; }
private float DistanceToPuck() { Vector3 puckPos = puck.transform.position; Vector3 paddlePos = paddle.transform.position; return Vector3.Distance(puckPos, paddlePos); }
private void Attack() { if (hasAttacked) return; // next steps }
agent.enabled = true; float dist = DistanceToPuck();
if (dist > leave.dangerRadius) { Vector3 newPos = puck.transform.position; newPos.z = paddle.transform.position.z; seek.target.transform.position = newPos; seek.enabled = true; return; }
hasAttacked = true; seek.enabled = false; Vector3 paddlePos = paddle.transform.position; Vector3 puckPos = puck.transform.position; Vector3 runPos = paddlePos - puckPos; runPos = runPos.normalized * 0.1f; runPos += paddle.transform.position; leave.target.transform.position = runPos; leave.enabled = true;
private void AttackReset() { float dist = DistanceToPuck(); if (hasAttacked && dist < leave.dangerRadius) return; hasAttacked = false; leave.enabled = false; }
private void Defend() { agent.enabled = true; seek.enabled = true; leave.enabled = false; Vector3 puckPos = puckBody.position; Vector3 puckVel = puckBody.velocity; Vector3 targetPos = Predict(puckPos, puckVel, 0); seek.target.transform.position = targetPos; }
private Vector3 Predict(Vector3 position, Vector3 velocity, int numHit) { if (numHit == maxHits) return position; // next steps }
RaycastHit[] hits = Physics.RaycastAll(position, velocity.normalized); RaycastHit hit;
foreach (RaycastHit h in hits) { string tag = h.collider.tag; // next steps }
if (tag.Equals(goalWallTag)) { position = h.point; position += (h.normal * puckWidth); return position; }
if (tag.Equals(sideWallTag)) { hit = h; position = hit.point + (hit.normal * puckWidth); Vector3 u = hit.normal; u *= Vector3.Dot(velocity, hit.normal); Vector3 w = velocity - u; velocity = w - u; break; } // end of foreach
foreach
loop:return Predict(position, velocity, numHit + 1);
The agent calculates the puck's next hits given its current velocity until the calculation results in the puck hitting the agent's wall. This calculation gives a point for the agent to move its paddle toward it. Furthermore, it changes to the attack mode when the puck is close to its paddle and is moving towards it. Otherwise, it changes to idle or defend depending on the new distance.
3.145.50.222