The player collecting carts to fire at The Biker
SiniCAR
SiniCAR is a remake of the classic arcade game Sinistar. The player finds themselves driving in a parking lot and must collect fuel to keep moving and shopping carts to fire at The Biker, the boss enemy who chases the player around the map.
Technologies
My Role
    I was the only engineer on this team, and worked with the team's producer on the game design. I was the only person on the project who had used Unity or Source Control software, so I dedicated time to teaching the team's artists and producer what they needed to know.
    It was a very interesting design challenge to recreate an existing arcade game while also adding a personal flair to make the game our own. After play testing and trying a few different mechanics, we came to the conclusion that removing all enemies except the boss and adding a fuel system helped to reduce the difficultly while still keeping gameplay interesting until the boss spawns. This was much more engaging and allowed players of lower skill levels to be able to quickly pick up the game and understand the basic systems in place.
Media
  Screenshots
The Biker chasing the player
The player firing the projectile at a truck so they can collect fuel to keep moving
A truck exploding after being shot enough and dropping fuel for the player
  Gameplay Video
Code
The code that handles all player inputs. This had to create the same feel as the original SiniStar arcade game,
and utilized unique ship physics and movement not typically used in space arcade games. These values had to be
tweaked purely based on game feel during play testing and comparing to the original. It also includes handling
the case when a user runs low on fuel.
if (fuelAmount <= 25)
{
speedReduction = lowFuelSpeedReduction;
}
else
{
speedReduction = 1.0f;
}
// Loop around if the player hits the max distance
if (transform.position.x > MapManager.xMax)
{
var loopedPosition = new Vector3(-MapManager.xMax, transform.position.y, transform.position.z);
transform.position = loopedPosition;
}
if (transform.position.x < -MapManager.xMax)
{
var loopedPosition = new Vector3(MapManager.xMax, transform.position.y, transform.position.z);
transform.position = loopedPosition;
}
if (transform.position.y > MapManager.yMax)
{
var loopedPosition = new Vector3(transform.position.x, -MapManager.yMax, transform.position.z);
transform.position = loopedPosition;
}
if (transform.position.y < -MapManager.yMax)
{
var loopedPosition = new Vector3(transform.position.x, MapManager.yMax, transform.position.z);
transform.position = loopedPosition;
}
if (Input.GetKey(KeyCode.W) && Input.GetKey(KeyCode.A))
{
// Facing NW
RotateOrApplyForce(45f);
}
else if (Input.GetKey(KeyCode.W) && Input.GetKey(KeyCode.D))
{
// Facing NE
RotateOrApplyForce(315f);
}
else if (Input.GetKey(KeyCode.S) && Input.GetKey(KeyCode.A))
{
// Facing SW
RotateOrApplyForce(135f);
}
else if (Input.GetKey(KeyCode.S) && Input.GetKey(KeyCode.D))
{
// Facing SW
RotateOrApplyForce(225f);
}
else if (Input.GetKey(KeyCode.W))
{
// Facing N
RotateOrApplyForce(0f);
}
else if (Input.GetKey(KeyCode.S))
{
// Facing S
RotateOrApplyForce(180f);
}
else if (Input.GetKey(KeyCode.A))
{
// Facing W
RotateOrApplyForce(90f);
}
else if (Input.GetKey(KeyCode.D))
{
// Facing E
RotateOrApplyForce(270f);
}
...
/// <summary>
/// Helper method that will compare the player's current angle with a desired angle
/// of movement. If the player is facing that angle they will move in that direction,
/// if not they will rotate to that position.
/// </summary>
/// <param name="desiredAngle"></param>
void RotateOrApplyForce(float desiredAngle)
{
var playerComponent = GetComponent<Rigidbody>();
var zRotation = playerComponent.transform.localRotation.eulerAngles.z;
bool isAngleWest = desiredAngle < 180f;
// Within margin of error, snap to the correct axis and begin applying force
if ((zRotation <= desiredAngle + marginOfError) && (zRotation >= desiredAngle - marginOfError))
{
transform.rotation = Quaternion.Euler(0f, 0f, desiredAngle);
GetComponent<Rigidbody>().AddForce(transform.up * movementSpeed * speedReduction * Time.deltaTime);
DrainFuel();
}
// The button was pressed but the player needs to rotate before applying force
else
{
// If the angle is < 180 different math is applied
if (isAngleWest)
{
if (zRotation > (desiredAngle + 1.0f) && zRotation <= (desiredAngle + 180.0f))
{
transform.Rotate(-transform.forward * rotationSpeed * Time.deltaTime);
}
else
{
transform.Rotate(transform.forward * rotationSpeed * Time.deltaTime);
}
}
else
{
if (zRotation >= (desiredAngle + 180.0f) % 360 && zRotation < (desiredAngle - 1.0f))
{
transform.Rotate(transform.forward * rotationSpeed * Time.deltaTime);
}
else
{
transform.Rotate(-transform.forward * rotationSpeed * Time.deltaTime);
}
}
}
}
The boss's AI movement. This was the part of the game that had the most iteration. The boss takes many hits,
while he kills the player in only one hit; it needed to be carefully designed and balanced to not be unfair
or frustrating to fight against. The movement began with a simple algorithm to move towards the player, and
eventually evolved into a chase where once the boss is close enough he begins rotating around the player
with an orbital decay to slowly encroach on the player. This system felt more fair while also creating a
great deal of tension once the boss was close enough to be seen.
// Loop around if the boss hits the max distance
if (transform.position.x > MapManager.xMax)
{
var loopedPosition = new Vector3(-MapManager.xMax, transform.position.y, transform.position.z);
transform.position = loopedPosition;
}
if (transform.position.x < -MapManager.xMax)
{
var loopedPosition = new Vector3(MapManager.xMax, transform.position.y, transform.position.z);
transform.position = loopedPosition;
}
if (transform.position.y > MapManager.yMax)
{
var loopedPosition = new Vector3(transform.position.x, -MapManager.yMax, transform.position.z);
transform.position = loopedPosition;
}
if (transform.position.y < -MapManager.yMax)
{
var loopedPosition = new Vector3(transform.position.x, MapManager.yMax, transform.position.z);
transform.position = loopedPosition;
}
if (player != null)
{
var distance = Vector3.Distance(transform.position, player.transform.position);
if (distance > rotationDistance)
{
transform.position = Vector3.MoveTowards(transform.position, player.transform.position, Time.deltaTime * speed);
if (currRotationDistance < rotationDistance)
{
// How close The Biker orbits decreases the longer you are away from him
currRotationDistance += rotationalDecay;
if (currRotationDistance >= rotationDistance)
{
currRotationDistance = rotationDistance;
}
}
}
else
{
transform.RotateAround(player.transform.position, new Vector3(0, 0, 1), rotationSpeed * Time.deltaTime);
var desiredPosition = (transform.position - player.transform.position).normalized * currRotationDistance + player.transform.position;
transform.position = Vector3.MoveTowards(transform.position, desiredPosition, Time.deltaTime * speed);
currRotationDistance -= rotationalDecay;
// Make sure the bike is facing upright
transform.rotation = new Quaternion(0, 0, 0, 0);
}
}
See the code repository HERE