Star Chaser
Keywords
Background Context
StarChaser is a bullet hell inspired game built for Android mobile devices only. The game was built using the Unity engine for my CS113 course (Computer Game Development) at UCI during Fall Quarter.
This was the first mobile game I developed which features one level and a boss. In the introductory level, your goal is to dodge and block using your shield any projectiles or enemies that come your way. At the end of the level, you will encounter a boss that has 3 phases of attack patterns. Each phase contains a set of movements that the boss can execute. Your job as the player piloting the ship is to learn how the boss operates and use what you’ve learned to take the beast down!
My responsibilities as one of the programmers in the team:
- design and program the Boss’s attack patterns and behavior
- program the player controller ship touch controls
- program the game HUD for displaying laser meter charge levels
- program shield button ability that also served as a radial cooldown indicator which notifies when the player can press on the button to activate
- program the pause menu
Tech Stack I used
- Unity C#
Approximate Completion Time
- 2 months
What challenges did I run into?
Extending or Retracting the Player’s Laser using Math
In LaserAbility.cs, I broke down the effect into 2 separate cases:
- Laser collides with a gameobject that has a collider component attached
- Laser does not collide with any gameobject
In order to determine if the laser hit something or not, I use Physics2D.Raycast() function
which takes in a Vector2 position and a Vector2 direction parameters.
If the return value of Physics2D.Raycast() returns a valid RaycastHit2D object, I know
that at least one game object got hit with the raycast. If it returns null, then no game
objects collided with the raycast.
Before proceeding deeper into the 2 separate cases, the laser art assets I have were broken into 3 parts: tip, body, and butt:
— add image of the 3 laser parts broken up and labelled —
The problems I solved for here were:
- Where should the tip of the laser sprite be relative to the player ship?
- How long should the body of the laser sprite be relative to the player ship?
Case 1: Laser collides with a gameobject via Raycast
— add image of laser overlapping with a game object here —
In this case, I take the RaycastHit2D's resulting game object's y position and set it to be the laser tip’s sprite position:
// hit is a Raycast2D obtained from Physics2D.Raycast()
float hitY = hit.collider.gameObject.transform.position.y;
tip.transform.position = new Vector3(tip.transform.position.x, hitY, tip.transform.position.z);
To calculate the laser body’s sprite height:
// used to adjust the height of the laser
float y_offset = 0.04f;
// hit is a Raycast2D obtained from Physics2D.Raycast()
float hitY = hit.collider.gameObject.transform.position.y;
/*
The math can be broken down to:
laser_height = laser_tip_local_y_position - B
B = C + D
C = laser_butt_local_y_position
D = laser_butt_sprite_bounding_box_height * laser_butt_sprite_y_local_scale
Lastly, to get the new y local scale of the laser body sprite, I divide the sum
of the newHeight and y_offset which is used to adjust the height of the laser body
and divide it by the laser body sprite's bounding box height.
*/
float newHeight = tip.transform.localPosition.y - (butt.transform.localPosition.y + butt.GetComponent<SpriteRenderer>().sprite.bounds.size.y * butt.transform.localScale.y);
float newYScale = (newHeight + y_offset) / body.GetComponent<SpriteRenderer>().sprite.bounds.size.y;
body.transform.localScale = new Vector3(body.transform.localScale.x, newYScale, body.transform.localScale.z);
Case 2: Laser collides with no game object
Since no gameobject collided with the raycast, the laser tip’s new y position will be the top edge of the camera which is calculated using the following formula:
//stretch past the screen vertically by rescaling the body's sprite
float newTipYPos = (Camera.main.orthographicSize * 2.5f) + Camera.main.transform.position.y;
Camera.main.orthographicSizeis half the height of the current game scene’s orthographic camera in Unity. I multiply by2.5here because I want the laser to extend outside the camera’s bounds.
The height calculation of the laser body sprite should be the same math logic as Case 1. The only thing that differs between both cases is where the laser tip y position is located.
— add image of laser not overlapping with any game objects here —
Programming and Designing the Boss in the span of 2 days
I had 2 days before the game was due and one of the last things needed was programming and designing the boss.
For the boss’s design, I was inpsired by old monster hunter’s design of not displaying an HP bar above the monster head and instead showing when the monster was weak where it would start limping away, have any of its body parts broken off, or would go in berserk mode.
I planned for the boss to have 3 different phases labeled as:
- Easy
- Angry
- Psycho
public enum Phase
{
EASY=0, //hands
ANGRY=1, //head
PSYCHO=2,// mouth
DEAD=3
}
The idea was the lower the hp the boss got, the harder it became to defeat.
Easy Phase
The purpose of this phase is for the player to learn how to charge up their laser meter by having the boss’s projectiles graze over the sides of the ship. An easy way to do this is to allow the ship’s side to graze over the projectile being shot down in the middle by the boss.
Once the laser meter is fully charged, the player will then soon discover through some trial and error that to attack the boss, they must fire their laser at both hands to enter the next phase.
I organized the code using a switch case statement to enumerate all the possible boss phases plus its dead state where the player wins the game upon defeating the boss:
void Update()
{
switch (bossPhase)
{
case Phase.EASY:
attackPattern1.enabled = true;
head.layer = (int)HitLayer.IGNORE_RAYCAST;
break;
case Phase.ANGRY:
head.layer = (int)HitLayer.DEFAULT;
attackPattern1.enabled = false;
attackPattern2.enabled = true;
break;
case Phase.PSYCHO:
attackPattern2.enabled = false;
head.SetActive(true);
head.layer = (int)HitLayer.IGNORE_RAYCAST;
head.GetComponent<Animator>().SetBool("canOpenMouth", true);
head.GetComponent<Collider2D>().enabled = false;
mouth.SetActive(true);
mouth.layer = (int)HitLayer.DEFAULT;
break;
case Phase.DEAD:
head.SetActive(false);
mouth.SetActive(false);
//goldStar.SetActive(true);
SceneManager.LoadScene("WinScene");
break;
}
}
Each case had different boss attack pattern combinations enabled. In the Phase.EASY mode, I put the logic in a component called BossAttackPattern1 and have it do 2 different kinds of attacks placed on a timer via coroutines. The logic goes as:
- Boss fires projectiles at player for
attackFrequencyseconds - After
attackFrequencyseconds passes, turn off the boss’s ability to fire projectiles and turn on the ability for the boss to move its arms towards the player.
All these behaviors I broke down into separate components which as a nice side effect made it easy to script boss behaviors without thinking too hard about the nitty gritty details
Angry Phase
The boss goes into the ANGRY phase when both of its hands are destroyed. Here I follow the same pattern as the previous section of turning on and off specific attack pattern behaviors at specific time intervals via Unity C# coroutines.
Psycho Phase
This is the hardest phase and where the boss is almost defeated! Here the boss’s head becomes invulnerable to laser damage and you must attack it in its mouth to defeat it.
What did I learn?
Programming Improvements
- For calculating the extension and retraction of the player’s laser, I could have stored it in a separate helper function to be reused when calculating the laser’s height as its used in 2 different cases.
- Setup object pooling for enemy projectiles being fired. Instead of instantiating and deleting projectiles as they go out of view, keeping a fixed amount of projectiles to recycle through may help as C# manages memory for us via garbage collection. The FPS hitches may be more apparent the more the projectiles are added/destroyed in game. This may be also become an issue on mobile devices with lower hardware capabilities. However, when I had this played by several people when my group and I presented the project, I didn’t see any players experience any hitches or slowdown in game performance. If I was to release on multiple platforms, I would target the device with the weakest specs (e.g. RAM, graphics card capabilities) and run performance tests to see if its going under the RAM values or the milliseconds per frame. That would be an exercise for a different project :D
UI/UX improvements
- Since this was designed for mobile devices, players may either hold the phone with their left or right hand. if they hold with their left hand, the laser bar and shield button would be blocked by their left hand; the opposite is true for right hand if the laser bar and shield button are placed on the right hand side. Adding an accessibility option to swap the position of where the laser bar and shield button ui are placed would help fix this issue.
Organizational Improvements
- Store experimental scripts for learning purposes in its own experimental folder to prevent confusion between scripts being actively used in the game vs ones that are being tested out for R&D purposes. In the future, especially when working with other peer programmers, is to collab and enforce common coding and file structure organization practices to help keep project tidy as creating a bigger project could make it difficult to find which component scripts are attached to which game object.
Game Features
- Charge up your ship’s laser by having enemy projectiles graze over the left and right sides of your ship
- Shield button to protect your ship temporarily from incoming enemy projectiles
- Touch controls to move ship around the game world
Game Credits
- Thank you Trey for the boss art and designs!
- Thank you Nick for Sound Design and feedback for Input Design for the mobile controls!
- Thank you Emil and Sai for design feedback for the mobile control designs I proposed during the early stages of the game’s development!