Time For Bed Dev. Log (Version 4)

Time For Bed was made in 48 hours by 6 people during Global Game Jam 2019 - Pittsburgh. There, it took home the award for Jammer’s Choice. a few months later, Swapnil Mengade, lead programmer of the project, submitted the game to the TOKYO SANDBOX game show, where it got selected for showing. COVID postponed the show, but we have since decided to port and optimize the 2-day game jam game into a more full-fledged release for both PC and mobile devices, prioritizing mobile devices. The game’s systems all work as a playable prototype, but we plan to build more levels and game types into the final release, so a fresh project was the logical next step. We chose Unity 2020 and a target audience: Mobile.

Player Controllers

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerController : MonoBehaviour
{
// public PlayerIndex playerIndex;
// GamePadState state;
// GamePadState prevState;
public int playerIndex;
public KeyCode up, down, right, left, jumpOrGrab;
private bool plusX, minusX;
private bool plusY, minusY;
private Rigidbody2D rigidbody;
private AudioSource audioSource;
public bool Movable;
public float Speed = 1.0f;
public Animator animator;
private bool facingLeft = true;
public AudioClip walkSoundClip;
//Parent
public bool Parent = false;
private PlayerController carriedKid = null;
private bool isCarryingKid = false;
public float GrabRange = 1.5f;
private bool inBedroom;
private bool onStairs;
public bool isCarryingBall = false;
public AudioClip grabSoundClip;
//Kids
public bool Grounded;
public float JumpVelocity = 3.0f;
public float DestroyRange = 1.5f;
private bool isJumping = false;
public bool isSleeping = false;
private float WakeupRange = 2.5f;
public GameObject afraidParticle;
public AudioClip jumpSoundClip;
public AudioClip sleepSoundClip;
public float throwVelocity = 3.0f;
public float throwUpVelocity = 3.0f;
void Start()
{
rigidbody = GetComponent<Rigidbody2D>();
audioSource = GetComponent<AudioSource>();
}
void Update()
{
//prevState = state;
//state = GamePad.GetState(playerIndex);
GetButtons();
GetAxis();
if (Parent)
{
KidsInRange();
}
if(!Parent)
{
WakeUpAKid();
}
}

Understandably, the game jam version of Time For Bed was made very quickly with little time for discussion of future work on the project. As a result, this version of the game uses a monolithic Player Controller script. To the left, you can see a small snippet of the original PlayerController script.

Immediately, I realized that this PlayerController script actually controlled two types of players with a simple public bool check: isParent (seen here on line 21 and checked for on lines 52 and 56). If checked true in the Unity editor, the player GameObject is a Parent, and if unchecked, the player is a Kid.

Instead of one monolithic script, I decided to use this important bool check to separate this PlayerController into two separate ones: a ParentController script and a KidController script (see below).

In creating the two new Controllers, I also wanted to remove the input checks from Update that you can see on lines 50 and 51. Using Unity 2020’s new Input System, the ParentController and KidController will wait for the player to use a stick or push a button before pinging the rest of the script, rather than checking every frame for inputs from the player.

Player statistics like movement speed, parent’s grab range, kid’s jump velocity, and parent’s throw velocity (lines 16, 24, 31, and 39 respectively) were all great details we were able to implement as soon as we did during the game jam. Without the amount of testing we were able to get done for the game, these mechanics would not have felt as great as they did at the end of the game jam. In early discussions for the mobile port, however, Swapnil and I knew we wanted to have different types of Parents and Kids with unique move speeds, jump velocities, or grabbing/freeing ranges. For this challenge, I decided to use Scriptable Objects for all of the player statistics.

using UnityEngine;
using UnityEngine.InputSystem;
public class ParentController : MonoBehaviour
{
public ParentStats parentStats;
private Rigidbody2D _rigidbody2D;
private KidFinder _kidFinder;
private bool _isHoldingKid = false;
private bool _onStairs;
private bool _inBedroom;
private Vector2 _moveInput;
private bool _grabButton;
void Awake()
{
_rigidbody2D = GetComponent<Rigidbody2D>();
_kidFinder = GetComponentInChildren<KidFinder>();
}
private void Update()
{
if (_grabButton)
{
GrabOrLayDownKid();
_grabButton = false;
}
}
void FixedUpdate()
{
ApplyMovement();
}
private void ApplyMovement()
{
if (_onStairs)
{
_rigidbody2D.velocity = new Vector2(_moveInput.x * parentStats.moveSpeed, _moveInput.y * parentStats.moveSpeed);
}
else
{
_rigidbody2D.velocity = new Vector2(_moveInput.x * parentStats.moveSpeed, _rigidbody2D.velocity.y);
}
}
private void GrabOrLayDownKid()
{
if (_isHoldingKid)
{
LayDownKid();
}
else
{
GrabKid();
}
}
private void GrabKid()
{
//if Kid in Range and Grab or throw used
if (_grabButton && _kidFinder.kidInRange && !_isHoldingKid)
{
//Grab Kid
_kidFinder.grabbableKid.transform.parent = transform;
_kidFinder.grabbableKid._rigidbody2D.simulated = false;
_kidFinder.grabbableKid.isMovable = false;
_isHoldingKid = true;
}
}
private void LayDownKid()
{
if (_grabButton && _isHoldingKid && _inBedroom)
{
//put kid in bed
_kidFinder.grabbableKid.Sleeping();
}
else
{
_kidFinder.grabbableKid. isMovable = true;
}
_kidFinder.grabbableKid.transform.parent = null;
_kidFinder.grabbableKid._rigidbody2D.simulated = true;
_isHoldingKid = false;
}
public void OnTriggerEnter2D(Collider2D other)
{
if (other.CompareTag("Stairs"))
{
_onStairs = true;
}
if (other.CompareTag("Bedroom"))
{
_inBedroom = true;
}
}
public void OnTriggerExit2D(Collider2D other)
{
if (other.CompareTag("Stairs"))
{
_onStairs = false;
}
if (other.CompareTag("Bedroom"))
{
_inBedroom = false;
}
}
public void Move(InputAction.CallbackContext context)
{
_moveInput = context.ReadValue<Vector2>();
}
public void GrabButton(InputAction.CallbackContext context)
{
//Debug.Log("GrabbedKid");
_grabButton = context.ReadValueAsButton();
}
}
using System;
using UnityEngine;
using UnityEngine.InputSystem;
public class KidController : MonoBehaviour
{
public KidStats kidStats;
public Rigidbody2D _rigidbody2D;
private bool _onGround;
private Vector2 _moveInput;
private bool _jumpButton;
private bool _onStairs = false;
public bool isMovable;
private bool _isSleeping;
public Transform feetPosition;
private float _checkRadius = 0.2f;
private LayerMask _groundLayerMask;
private bool _isJumping;
private float _jumpTimeCounter;
//public bool _isGrabbable = false;
private void Awake()
{
_rigidbody2D = GetComponent<Rigidbody2D>();
_groundLayerMask = LayerMask.GetMask("Ground");
}
private void Start()
{
isMovable = true;
}
private void FixedUpdate()
{
ApplyMovement();
ApplyJump();
_onGround = Physics2D.OverlapCircle(feetPosition.position, _checkRadius, _groundLayerMask);
}
private void ApplyMovement()
{
if (isMovable)
{
if (_onStairs )
{
_rigidbody2D.velocity = new Vector2(_moveInput.x * kidStats.moveSpeed, _moveInput.y * kidStats.moveSpeed);
}
else
{
_rigidbody2D.velocity = new Vector2(_moveInput.x * kidStats.moveSpeed, _rigidbody2D.velocity.y);
}
}
}
private void ApplyJump()
{
if (_jumpButton && _onGround)
{
_isJumping = true;
_jumpTimeCounter = kidStats.jumpTime;
_rigidbody2D.velocity = Vector2.up * kidStats.jumpForce;
}
if (_jumpButton && _isJumping)
{
if (_jumpTimeCounter > 0)
{
_rigidbody2D.velocity = Vector2.up * kidStats.jumpForce;
_jumpTimeCounter -= Time.deltaTime;
}
else
{
_isJumping = false;
_jumpButton = false;
}
}
else
{
_isJumping = false;
}
}
public void Sleeping()
{
_isSleeping = true;
isMovable = false;
//TODO:
//animator.SetTrigger("Sleep");
//play audio
}
private void WakeUpSibling()
{
//When waking up other kids in the game
throw new NotImplementedException();
}
private void WokenUp()
{
_isSleeping = false;
isMovable = true;
//animator.SetTrigger("WokenUp");
}
public void OnTriggerEnter2D(Collider2D other)
{
if (other.CompareTag("Stairs"))
{
_onStairs = true;
}
if (other.CompareTag("Kid") && _isSleeping)
{
other.GetComponent<KidController>().WokenUp();
}
}
public void OnTriggerExit2D(Collider2D other)
{
if (other.CompareTag("Stairs"))
{
_onStairs = false;
}
}
public void Move(InputAction.CallbackContext context)
{
Debug.Log("Moving");
_moveInput = context.ReadValue<Vector2>();
}
public void Jump(InputAction.CallbackContext context)
{
_jumpButton = context.ReadValueAsButton();
}
}
view raw KidController hosted with ❤ by GitHub

Parent can take in scriptable object variables, walk, climb stairs, detect when kid is nearby, grab kid, put down kid in bedroom.

For Kids, these stats are things like jump velocity and move speed. Kid can take in scriptable object variables, walk, climb stairs, jump, and wake up other kids.





Input System:
Unity’s new Input System is great and allows way more flexibility through Actions rather than waiting for a KeyCode press during every frame in Update. Instead, the new Input System allows you to dedicate all kinds of inputs from different devices like mouse/keyboard, controllers, and mobile devices into one single action you check for in your scripts. I’ve come to the realization that it allows for much faster growth of a project in case buttons need to be moved around, but the action will still stay the same in your code. I also decided to prototype a select screen because I couldn’t playtest both a parent and kid. I used a PS4 controller and mouse and keyboard for players one and two, respectively.

Main Menu

Note+Aug+27%2C+2020+9_52_33+PM.jpg

Cinemachine follow cams.

up/down functionality

This script takes one UI Canvas array and one Cinemachine Virtual Camera array. On player input, it cycles up or down through the arrays. Since they must always cycle together, a single integer (currentCanvasAndVCam) is used for both arrays. EDIT: After learning a bit more about the UI functions in the Unity editor alone, I decided to incorporate more editor functions to enable and disable different game objects within the editor. I use these in conjunction with the private int in the StartMenu script (now called currentMenuAndVCam)

using UnityEngine;
using UnityEngine.InputSystem;
public class StartMenu : MonoBehaviour
{
public GameObject[] canvases = new GameObject[4];
public GameObject[] vCams = new GameObject[4];
private Vector2 _moveInput;
private int _currentCanvasAndVCam;
private void Start()
{
for (int i = 0; i < 4; i++)
{
canvases[i].SetActive(false);
vCams[i].SetActive(false);
}
_currentCanvasAndVCam = 0;
canvases[0].SetActive(true);
vCams[0].SetActive(true);
}
public void Move(InputAction.CallbackContext context)
{
_moveInput = context.ReadValue<Vector2>();
//context.performed checks only for OnButtonDown
if (context.performed)
{
NavigateMenus();
}
}
private void NavigateMenus()
{
//if player pushes up
if (_moveInput.y > 0.1)
{
_currentCanvasAndVCam++;
if (_currentCanvasAndVCam > 3)
{
_currentCanvasAndVCam = 0;
}
for (int i = 0; i < 4; i++)
{
canvases[i].SetActive(false);
vCams[i].SetActive(false);
}
canvases[_currentCanvasAndVCam].SetActive(true);
vCams[_currentCanvasAndVCam].SetActive(true);
}
//if player pushes down
else if(_moveInput.y < -0.1)
{
_currentCanvasAndVCam--;
if (_currentCanvasAndVCam < 0)
{
_currentCanvasAndVCam = 3;
}
for (int i = 0; i < 4; i++)
{
canvases[i].SetActive(false);
vCams[i].SetActive(false);
}
canvases[_currentCanvasAndVCam].SetActive(true);
vCams[_currentCanvasAndVCam].SetActive(true);
}
}
}
view raw StartMenu hosted with ❤ by GitHub

Blend between cameras on player input (move action and back to the bottom floor when going from settings to play). Ask Swap Why I get errors and how to cycle between a list. Simple. Check for it.

For simplicity’s sake, Swapnil also recommended I convert both currentVCam and currentCanvas into one single integer. This new one is called _currentCanvasAndVCam.

For left and right functionality, I used Unity’s Navigation UI feature, explicitly telling each button where to go.

The Local/Online button toggles the Play button’s features. If Local is set, then the play button will take the player to the local multiplayer screen (Name, Host game, Available games nearby, or back to main menu). If online is set, then the play button will take the player to the online multiplayer screen (Name, Host [Create game], Public [find game], private game [enter code], or back to main menu).

Simple AI with state pattern (this pattern is used in computer programming to encapsulate varying behavior for the same object based on its internal state) (a little too advanced for me… Instead, I just used a simple animation of them walking back and forth until Swap gets AI up and running)

For the Settings menu, I wanted an audio mixer to control the volume settings. But it wasn’t actually working so I found this YouTube video which did the trick: https://www.youtube.com/watch?v=xNHSGMKtlv4

Set minimum value of sliders from 0 to 0.001. Added Mathf.Log10 because mixers are set to decibels.

Mobile Devices

Game Controller

The simplicity of the game made porting this one pretty simple. The only trouble I had was in Swapnil’s original implementation of AllCHildren Asleep. His read as backwards to me, but after asking him why he implemented that way, here is his explanation:

Loading Screen

Created an enum script for more user-friendly build index management.