Unity Physics: Creating a Bowling Game Part 2
The fun and games of creating my Free Play scene are over. It’s time to get down to business on the Game Scene, and it is going to be a lot more complicated. Let’s get to it!
Game Play Objectives
The Game Scene consists of ten throw patterns that the player can progress through. To complete a throw pattern, you must avoid hitting all red pins, and collect all green pins while throwing a strike. Whenever the player completes three consecutive objectives, the reward is being “on fire” and having a throw with the exploding ball. The advantage here is being able to throw an easy strike on your next turn.
If you miss one of the objectives and fail the throw, you can try again infinitely. The only end game here is finishing all 10 throw patterns, or quitting the game.
Throwing the ball into the room environment will not count against you, and you can throw again. This feature comes in handy for throwing a ball at the jukebox to change the music.
Observer Pattern
For the most part, this game is entirely event driven. The player is communicating with the Physics Manager in FixedUpdate when drawing the trajectory line, so I chose a direct connection between those classes over firing off rapid events. The same can be said about the player and the UI Manager when it comes to updating the force fill UI bar at framerate.
Other than that, everything is event driven, with each object reacting as needed to get their respective jobs done.
Here are the variables in the Game Manager.
public class GameManager : MonoBehaviour
{
private int _throwCount;
private int _consecutiveThrows;
private int _totalThrows;
private bool _turnComplete = false;
private bool _objectivesComplete = false;
private bool _isGameOver = false;
private int _mainMenuScene = 0, _thisScene = 2;
public static event Action<int> onChallengeStart;
public static event Action onChallengeComplete;
public static event Action onGameComplete;
public static event Action<bool, bool> onDisplayResults;
public static event Action onIsOnFire;
}
Throw System
The Throw System is designer friendly. The Pin Manager class has references to Pin Positions through Scriptable Objects. Each scriptable object holds an array of positions, that can be easily assigned in the inspector. The Pin Positions is used to create pin positions for each throw, and for each type of pin. The standard 10 pins always remain in their starting positions, so the pin positions are used here for the pins to avoid and collect. I am using an array of pin positions, and I use the length of the collectable pin positions to determine when the game is over. If I want to extend the game, all I have to do is create new scriptable objects and assign the positions for the next throw, then drag those into the Pin Manager and that’s it!
Pin System
Once the Game Manager knows the throw has been completed, it invokes an event for the Pin Manager to calculate the results. Each Pin waits until it stops wiggling before checking it’s current rotation and position vs it’s starting rotation and position. If the pin has moved or rotated past the threshold, it will tell the Pin Manager it has been knocked down. Just using the movement threshold was not enough, being some pins fall over in place and virtually do not move at all. Similarly, just checking the rotation threshold did not work, as some pins when hit with high velocity would get pushed to the back of the box, well away from their starting position, but remain upright against the box enough to determine it was not knocked down. Once the Pins have checked their status, the Pin Manager gathers the results to calculate.
The green Collectable Pin sends an event message to the Pin Manager when it is collected, and the Pin Manager compares the collected amount vs the total available for that throw.
The red Avoid Pin simply lets the Pin Manager know that it has been hit, and the Pin Manager fails the objective.
private IEnumerator PinCheck()
{
yield return _pinCheckTime;
while (_rb.angularVelocity.sqrMagnitude > _minAngularVelocity)
{
yield return _pinCheckTime;
}
//used to check how far the pin has moved and rotated from it's starting position and rotation
_angle = Quaternion.Angle(_rb.rotation, _startingRotation);
_distance = Vector3.Distance(_rb.position, _startingPosition);
Debug.Log("Angle " + _angle);
Debug.Log("Distance " + _distance);
if (_angle > _rotationTolerance | _distance > _positionTolerance)
{
_isDown = true;
//Debug.Log("Pin DOWN");
}
onPinComplete?.Invoke();
}
Throw Patterns
The Pin Manager holds an array of scriptable objects. Each of these scriptable objects is a Vector3 array where new positions can be assigned in the inspector. Every time a throw pattern is completed, the Game Manager will increment a throw count value, then the Pin Manager uses that as the element in it’s positions array. The current array of positions is added to a list, which is what actually get’s used to instantiate the pins. Every time the throw restarts or advances, the lists are cleared for the next throw pattern.
#region Private Methods
private void SetPins(int throwCount)
{
//collectable pins
foreach (Vector3 position in _collectablePinPositionsSO[throwCount].positions)
{
_collectablePinPositions.Add(position);
}
for (int i = 0; i < _collectablePinPositions.Count; i++)
{
Instantiate(_collectablePin, _collectablePinPositions[i], Quaternion.identity, _pinParent);
}
//avoid pins
foreach (Vector3 position in _avoidPinPositionsSO[throwCount].positions)
{
_avoidPinPositions.Add(position);
}
for (int i = 0; i < _avoidPinPositions.Count; i++)
{
Instantiate(_avoidPin, _avoidPinPositions[i], Quaternion.identity, _pinParent);
}
//moving pins
foreach (Vector3 position in _movingPinPositionsSO[throwCount].positions)
{
_movingPinPositions.Add(position);
}
for (int i = 0; i < _movingPinPositions.Count; i++)
{
Instantiate(_movingPin, _movingPinPositions[i], Quaternion.identity, _pinParent);
}
}
private void CheckPins()
{
foreach (Pin pin in _tenPins)
{
_pinCheck = pin.PinDown();
if (_pinCheck)
_pinsDown++;
}
CalculateResults();
}
private void CalculateResults()
{
if (_pinsCollected == _collectablePinPositions.Count)
_isCollected = true;
if (_pinsDown == 10)
_isStrike = true;
onResultsCalculated?.Invoke(_isStrike, _isCollected, _redPinHit, _pinsCollected, _collectablePinPositions.Count, _pinsDown);
}
#endregion
UI
The UI consists of an always available controls panel, a ball panel, a stats panel, and an assortment of buttons.
In between throws, the cursor is enabled so the player can click the buttons. While the controls and ball panels are available in both game scenes, the stats panel and animated strike text are exclusive to the game scene. After the results of a throw have been calculated, the stats panel displays how many collectable pins were collected vs the total, how many pins you knocked down out of 10, and if you hit a red pin or not. For each objective completed successfully, a single star is awarded and displayed.
Lighting
I was torn between making the scene night or day, so i did both! The free play scene hosts evening ambience, while the game scene takes place on a sunny afternoon. Both scenes have baked lighting, with the exception of two real-time point lights, one for the pins and one for the main room.
Audio
The audio in this game is has not been forgotten! There are four soundtracks to change between when hitting the jukebox with a ball. The avoid pins play a single pin hit sound, the collectable pins play a collected sound, and the 10 pins play a strike sound when hit. The ball has an impact sound when it first hits the floor, then the rolling sound loops in 3D space providing a more realistic soundscape. The exploding ball is not forgotten here either, providing a nice boom sound on impact.
If you can make it to the final two throws, this is what you have to look forward to. Good luck!
Feel free to download the game for free here!
Any feedback is welcome and thanks for reading! Best of luck bowling legends.