Devising a table-football competitor

Another common table game that has made its way into the digital realm is table football. In this recipe, we will create a competitor, imitating the way a human plays the game and using some techniques that emulate human senses and limitations.

Getting ready

In this recipe, we will use the knowledge gained from Chapter 5, Agent Awareness, and the emulation of vision.

First, it is important to have a couple of enum data structures, as shown in the following code:

public enum TFRAxisCompare
{
    X, Y, Z
}

public enum TFRState
{
    ATTACK, DEFEND, OPEN
}

How to do it…

This is a very extensive recipe. We'll build a couple of classes, one for the table-football bar and the other for the main AI agent that handles the bars, as follows:

  1. Create a class for the bar that will be handled by the AI:
    using UnityEngine;
    using System.Collections;
    
    public class TFRBar : MonoBehaviour
    {
        [HideInInspector]
        public int barId;
        public float barSpeed;
        public float attackDegrees = 30f;
        public float defendDegrees = 0f;
        public float openDegrees = 90f;
        public GameObject ball;
        private Coroutine crTransition;
        private bool isLocked;
        // next steps
    }
  2. Implement the Awake function:
    void Awake()
    {
        crTransition = null;
        isLocked = false;
    }
  3. Define the function for setting the state of the bar:
    public void SetState(TFRState state, float speed = 0f)
    {
        // next steps
    }
  4. Check whether it is locked (after beginning a movement). This is optional:
    // optional
    if (isLocked)
        return;
    isLocked = true;
  5. Validate the speed:
    if (speed == 0)
        speed = barSpeed;
    float degrees = 0f;
  6. Validate the state and make a decision out of it:
    switch(state)
    {
        case TFRState.ATTACK:
            degrees = attackDegrees;
            break;
        default:
        case TFRState.DEFEND:
            degrees = defendDegrees;
            break;
        case TFRState.OPEN:
            degrees = openDegrees;
            break;
    }
  7. Execute the transition:
    if (crTransition != null)
        StopCoroutine(crTransition);
    crTransition = StartCoroutine(Rotate(degrees, speed));
  8. Define the function for rotating the bar:
    public IEnumerator Rotate(float target, float speed)
    {
        // next steps
    }
  9. Implement the internal body for the transition:
    while (transform.rotation.x != target)
    {
        Quaternion rot = transform.rotation;
        if (Mathf.Approximately(rot.x, target))
        {
            rot.x = target;
            transform.rotation = rot;
        }
        float vel = target - rot.x;
        rot.x += speed * Time.deltaTime * vel;
        yield return null;
    }
  10. Restore the bar to its default position:
    isLocked = false;
    transform.rotation = Quaternion.identity;
  11. Implement the function for moving the bar from side to side:
    public void Slide(float target, float speed)
    {
        Vector3 targetPos = transform.position;
        targetPos.x = target;
        Vector3 trans = transform.position - targetPos;
        trans *= speed * Time.deltaTime;
        transform.Translate(trans, Space.World);
    }
  12. Create the class for the main AI:
    using UnityEngine;
    using System.Collections;
    using System.Collections.Generic;
    
    public class TFRival : MonoBehaviour
    {
    
        public string tagPiece = "TFPiece";
        public string tagWall = "TFWall";
        public int numBarsToHandle = 2;
        public float handleSpeed;
        public float attackDistance;
        public TFRAxisCompare depthAxis = TFRAxisCompare.Z;
        public TFRAxisCompare widthAxis = TFRAxisCompare.X;
        public GameObject ball;
        public GameObject[] bars;
        List<GameObject>[] pieceList;
        // next
    }
  13. Implement the Awake function for initializing the piece list:
    void Awake()
    {
        int numBars = bars.Length;
        pieceList = new List<GameObject>[numBars];
        for (int i = 0; i < numBars; i++)
        {
            pieceList[i] = new List<GameObject>();
        }
    }
  14. Start implementing the Update function:
    void Update()
    {
        int[] currBars = GetNearestBars();
        Vector3 ballPos = ball.transform.position;
        Vector3 barsPos;
        int i;
        // next steps
    }
  15. Define the status for each bar, depending on the ball's position:
    for (i = 0; i < currBars.Length; i++)
    {
        GameObject barObj = bars[currBars[i]];
        TFRBar bar = barObj.GetComponent<TFRBar>();
        barsPos = barObj.transform.position;
        float ballVisible = Vector3.Dot(barsPos, ballPos);
        float dist = Vector3.Distance(barsPos, ballPos);
        if (ballVisible > 0f && dist <= attackDistance)
            bar.SetState(TFRState.ATTACK, handleSpeed);
        else if (ballVisible > 0f)
            bar.SetState(TFRState.DEFEND);
        else
            bar.SetState(TFRState.OPEN);
    }
  16. Implement the OnGUI function. This will handle the prediction at 30 frames per second:
    public void OnGUI()
    {
        Predict();
    }
  17. Define the prediction function with its member values:
    private void Predict()
    {
        Rigidbody rb = ball.GetComponent<Rigidbody>();
        Vector3 position = rb.position;
        Vector3 velocity = rb.velocity.normalized;
        int[] barsToCheck = GetNearestBars();
        List<GameObject> barsChecked;
        GameObject piece;
        barsChecked = new List<GameObject>();
        int id = -1;
        // next steps
    }
  18. Define the main loop for checking the ball's trajectory:
    do
    {
        RaycastHit[] hits = Physics.RaycastAll(position, velocity.normalized);
        RaycastHit wallHit = null;
        foreach (RaycastHit h in hits)
        {
            // next steps
        }
    
    } while (barsChecked.Count == numBarsToHandle);
  19. Get the object of the collision and check whether it is a bar and whether it has been checked already:
    GameObject obj = h.collider.gameObject;
    if (obj.CompareTag(tagWall))
        wallHit = h;
    if (!IsBar(obj))
        continue;
    if (barsChecked.Contains(obj))
        continue;
  20. Check, if it is a bar, whether it is among those closest to the ball:
    bool isToCheck = false;
    for (int i = 0; i < barsToCheck.Length; i++)
    {
        id = barsToCheck[i];
        GameObject barObj = bars[id];
        if (obj == barObj)
        {
            isToCheck = true;
            break;
        }         
    }
    if (!isToCheck)
        continue;
  21. Get the bar collision point and calculate the movement for blocking the ball with the closest piece:
    Vector3 p = h.point;
    piece = GetNearestPiece(h.point, id);
    Vector3 piecePos = piece.transform.position;
    float diff = Vector3.Distance(h.point, piecePos);
    obj.GetComponent<TFRBar>().Slide(diff, handleSpeed);
    barsChecked.Add(obj);
  22. Otherwise, recalculate with the wall's hitting point:
    C
  23. Create the function for setting the pieces to the proper bar:
    void SetPieces()
    {
        // next steps
    }
  24. Create a dictionary for comparing the pieces' depth:
    // Create a dictionary between z-index and bar
    Dictionary<float, int> zBarDict;
    zBarDict = new Dictionary<float, int>();
    int i;
  25. Set up the dictionary:
    for (i = 0; i < bars.Length; i++)
    {
        Vector3 p = bars[i].transform.position;
        float index = GetVectorAxis(p, this.depthAxis);
        zBarDict.Add(index, i);
    }
  26. Start mapping the pieces to the bars:
    // Map the pieces to the bars
    GameObject[] objs = GameObject.FindGameObjectsWithTag(tagPiece);
    Dictionary<float, List<GameObject>> dict;
    dict = new Dictionary<float, List<GameObject>>();
  27. Assign pieces to their proper dictionary entry:
    foreach (GameObject p in objs)
    {
        float zIndex = p.transform.position.z;
        if (!dict.ContainsKey(zIndex))
            dict.Add(zIndex, new List<GameObject>());
        dict[zIndex].Add(p);
    }
  28. Define the function for getting a bar's index, given a position:
    int GetBarIndex(Vector3 position, TFRAxisCompare axis)
    {
        // next steps
    }
  29. Validate it:
    int index = 0;
    if (bars.Length == 0)
        return index;
  30. Declare the necessary member values:
    float pos = GetVectorAxis(position, axis);
    float min = Mathf.Infinity;
    float barPos;
    Vector3 p;
  31. Traverse the list of bars:
    for (int i = 0; i < bars.Length; i++)
    {
        p = bars[i].transform.position;
        barPos = GetVectorAxis(p, axis);
        float diff = Mathf.Abs(pos - barPos);
        if (diff < min)
        {
            min = diff;
            index = i;
        }
    }
  32. Retrieve the found index:
    return index;
  33. Implement the function for calculating the vector axis:
    float GetVectorAxis(Vector3 v, TFRAxisCompare a)
    {
        if (a == TFRAxisCompare.X)
            return v.x;
        if (a == TFRAxisCompare.Y)
            return v.y;
        return v.z;
    }
  34. Define the function for getting the nearest bars to the ball:
    public int[] GetNearestBars()
    {
        // next steps
    }
  35. Initialize all the necessary member variables:
    int numBars = Mathf.Clamp(numBarsToHandle, 0, bars.Length);
    Dictionary<float, int> distBar;
    distBar = new Dictionary<float, int>(bars.Length);
    List<float> distances = new List<float>(bars.Length);
    int i;
    Vector3 ballPos = ball.transform.position;
    Vector3 barPos;
  36. Traverse the bars:
    for (i = 0; i < bars.Length; i++)
    {
        barPos = bars[i].transform.position;
        float d = Vector3.Distance(ballPos, barPos);
        distBar.Add(d, i);
        distances.Add(d);
    }
  37. Sort the distances:
    distances.Sort();
  38. Get the distances and use the dictionary in an inverse way:
    int[] barsNear = new int[numBars];
    for (i = 0; i < numBars; i++)
    {
        float d = distances[i];
        int id = distBar[d];
        barsNear[i] = id;
    }
  39. Retrieve the bar IDs:
    return barsNear;
  40. Implement the function for checking whether a given object is a bar:
    private bool IsBar(GameObject gobj)
    {
        foreach (GameObject b in bars)
        {
            if (b == gobj)
                return true;
        }
        return false;
    }
  41. Start implementing the function for retrieving the closest piece of a bar, given a position:
    private GameObject GetNearestPiece(Vector3 position, int barId)
    {
        // next steps
    }
  42. Define the necessary member variables:
    float minDist = Mathf.Infinity;
    float dist;
    GameObject piece = null;
  43. Traverse the list of pieces and calculate the closest one:
    foreach (GameObject p in pieceList[barId])
    {
        dist = Vector3.Distance(position, p.transform.position);
        if (dist < minDist)
        {
            minDist = dist;
            piece = p;
        }
    }
  44. Retrieve the piece:
    return piece;

How it works…

The table-football competitor draws on the skills developed from the air-hockey rival. This means casting rays to get the trajectory of the ball and moving the nearest bar considering the pieces. It also moves the bar, depending on whether the rival is attacking or defending, so that it can block the ball or let it go further.

See also

  • The Seeing using a collider-based system recipe in Chapter 5, Agent Awareness
..................Content has been hidden....................

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