Building an air-hockey rival

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.

Getting ready

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:

Getting ready

Finally, it is important to create an enum type for handling the rival's state:

public enum AHRState
{
    ATTACK,
    DEFEND,
    IDLE
}

How to do it…

This is a long class, so it is important to carefully follow these steps:

  1. Create the rival's class:
    using UnityEngine;
    using System.Collections;
    
    public class AirHockeyRival : MonoBehaviour
    {
        // next steps
    }
  2. Declare the public variables for setting it up and fine-tuning it:
    public GameObject puck;
    public GameObject paddle;
    public string goalWallTag = "GoalWall";
    public string sideWallTag = "SideWall";
    [Range(1, 10)]
    public int maxHits;
  3. Declare the private variables:
    float puckWidth;
    Renderer puckMesh;
    Rigidbody puckBody;
    AgentBehaviour agent;
    Seek seek;
    Leave leave;
    AHRState state;
    bool hasAttacked;
  4. Implement the 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();
    }
  5. Declare the Update member function. The following steps will define its body:
    public void Update()
    {
        // next steps
    }
  6. Check the current state and call the proper functions:
    switch (state)
    {
        case AHRState.ATTACK:
            Attack();
            break;
        default:
        case AHRState.IDLE:
            agent.enabled = false;
            break;
        case AHRState.DEFEND:
            Defend();
            break;
    }
  7. Call the function for resetting the active state for hitting the puck:
    AttackReset();
  8. Implement the function for setting up the state from external objects:
    public void SetState(AHRState newState)
    {
        state = newState;
    }
  9. Implement the function for retrieving the distance from paddle to puck:
    private float DistanceToPuck()
    {
        Vector3 puckPos = puck.transform.position;
        Vector3 paddlePos = paddle.transform.position;
        return Vector3.Distance(puckPos, paddlePos);
    }
  10. Declare the member function for attacking. The following steps will define its body:
    private void Attack()
    {
        if (hasAttacked)
            return;
        // next steps
    }
  11. Enable the agent component and calculate the distance to puck:
    agent.enabled = true;
    float dist = DistanceToPuck();
  12. Check whether the puck is out of reach. If so, just follow it:
    if (dist > leave.dangerRadius)
    {
        Vector3 newPos = puck.transform.position;
        newPos.z = paddle.transform.position.z;
        seek.target.transform.position = newPos;
        seek.enabled = true;
        return;    
    }
  13. Attack the puck if it is within reach:
    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;
  14. Implement the function for resetting the parameter for hitting the puck:
    private void AttackReset()
    {
        float dist = DistanceToPuck();
        if (hasAttacked && dist < leave.dangerRadius)
            return;
        hasAttacked = false;
        leave.enabled = false;
    }
  15. Define the function for defending the goal:
    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;
    }
  16. Implement the function for predicting the puck's position in the future:
    private Vector3 Predict(Vector3 position, Vector3 velocity, int numHit)
    {
        if (numHit == maxHits)
            return position;
        // next steps
    }
  17. Cast a ray, given the position and the direction of the puck:
    RaycastHit[] hits = Physics.RaycastAll(position, velocity.normalized);
    RaycastHit hit;
  18. Check the hit results:
    foreach (RaycastHit h in hits)
    {
        string tag = h.collider.tag;
        // next steps
    }
  19. Check whether it collides with the goal wall. Base case:
    if (tag.Equals(goalWallTag))
    {
        position = h.point;
        position += (h.normal * puckWidth);
        return position;
    }
  20. Check whether it collides with a side wall. Recursive case:
    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
  21. Enter the recursive case. This is done from the foreach loop:
    return Predict(position, velocity, numHit + 1);

How it works…

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.

See also

  • Chapter 1, Movement recipes Pursuing and evading and Arriving and leaving recipes
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.223.237.131