Notifications
Article
Smashing Crystals
Updated a month ago
160
6
Description
Smashing Crystals is a small game concept where you control Aura, a small stellar nebula who has the goal of becoming a star and shine like no other star has shined before, but to become one she needs to fuse with a galactic crystal, item that only appears every 10,000 years at the top of the highest mountain in the Tulum planet.
The path to obtain the galactic crystal is filled with all kinds of challenges and trials, only stellar nebulas who are brave enough and reach the top of the mountain are able to become a star, and those who fail their mission are not allowed to enter the planet ever again.

Animation

I wanted the game scene to feel organic and lively, so I decided to animate most of the elements that are seen on screen, for example, the grass, the flowers, stars, etc.
Animations were created using the Unity animation 2D package, below are some examples of the rigs:
Below are the animations of the previous rigs:

Applying delays to animations

In order to prevent animations from feeling robotic or unnatural I applied a slight delay to the animation start of the different objects in the scene, to better illustrate this let’s see the following example where all animations start at the same time:
As you probably noticed it looks a bit weird, now let’s apply a different delay (a few milliseconds) to the start of the animation of every object and see how it looks:
There you go, much better!
To achieve this I created a small base component that handled the delay using StartCoroutine and WaitForSeconds:
using System; using System.Collections; using System.Collections.Generic; using UnityEngine; public abstract class BaseAnimator : MonoBehaviour { [Range(0f, 3f)] public float delay = 0f; public virtual void StartAnimation() { StartCoroutine(Delay(delay, PrepareAnimation)); } private IEnumerator Delay(float time, Action action) { yield return new WaitForSeconds(time); action(); } protected abstract void PrepareAnimation(); }
Then now I can apply delays to any kind of animations (unity animation controllers, animations done with code or animations using any package from the asset store), I just need to create a component that inherits from BaseAnimator and implement the PrepareAnimation method, for example here is the code for the flowers animations:
using System.Collections; using System.Collections.Generic; using UnityEngine; [RequireComponent(typeof(Animator))] public class FlowerAnimatorController : BaseAnimator { [SerializeField] [Range(0f, 5f)] private float speed = 1f; private Animator animator; // Use this for initialization void Start() { animator = GetComponent<Animator>(); animator.speed = speed; StartAnimation(); } protected override void PrepareAnimation() { animator.SetBool("move", true); } }

Creating the level

The level was created with Unity sprite shape:
Once I was happy with the shape I began to add details such as the grass and flowers, since I wanted the grass to be animated I couldn't include it as part of the sprite shape and instead I separated it in a different game object with its own parameters such as rotation, animation delay, etc:
At the beginning I was doing this manually but this was very time consuming since I needed to adjust the parameters of each individual game object (making sure the delay for the animations was adjusted on each grass object to make the animation look more natural) so I needed to figure out a way to do this in a more efficient way.
Unity provides a example script to attach objects to nodes on the spline of a sprite shape but unfortunately this didn’t suit my needs since I wanted the grass objects to follow the curve’s path and also I still had to place each object manually.
To solve this I created a tool that looped over the curve’s nodes, calculated the position of a point on the curve between two nodes and then instantiated prefabs based on a spacing parameter defined in the inspector:
Additionally the tool automatically randomizes each grass object animation parameters based on a list of values defined in the inspector:
Great now there is no more manual editing!
There were two key pieces I needed to figure out in order to build this tool:
1. Understand how Bézier curves work
Sprite shapes paths are made of Bézier curves so I needed to understand how they worked and fortunately I found this great article that explains really well how Bézier curves work. After that I used the BezierUtility.BezierPoint function to calculate the position of a point on the curve between two nodes in the sprite shape.
2. Find how to calculate the normal of a point in the curve
I wanted that grass game objects were only placed on certain parts of the sprite shape (only in the ground, not in the laterals nor the lower bounds) as demonstrated in the following image:
I needed to get the curve normal and in order to do so I did a small “trick”, based on the current point position I slightly looked ahead and got the position of the next point, then calculated the angle between those two points and rotated it by 90 degrees:
And finally after adding a condition the grass objects were instantiated only if the normal was between the min and max angles defined in the inspector!
Here is the code for the tool, it is divided in two scripts:
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.U2D; [RequireComponent(typeof(SpriteShapeController))] [RequireComponent(typeof(GrassRandomizer))] [ExecuteInEditMode] public class GrassPopulator : MonoBehaviour { private const float TimeLookAheadOffset = 0.05f; [SerializeField] private List<GameObject> grassPrefabs; [SerializeField] private float minAngle = 45f; [SerializeField] private float maxAngle = 135f; [SerializeField] [Range(0f, 0.5f)] private float spacing = 0.1f; private SpriteShapeController spriteShape; private GrassRandomizer grassRandomizer; private void Awake() { spriteShape = GetComponent<SpriteShapeController>(); grassRandomizer = GetComponent<GrassRandomizer>(); } public void Populate() { int pointCount = spriteShape.spline.GetPointCount(); float t = 0f; for (int i = 0; i < pointCount; i++) { float currentT = 0f; int nextPointIndex = i < pointCount - 1? i + 1 : 0; while (currentT < 1f) { Vector3 currentPosition = GetPositionInSpline(i, nextPointIndex, t); Vector3 lookAheadPosition = GetPositionInSpline(i, nextPointIndex, t + TimeLookAheadOffset); float normalAngle = GetNormal(currentPosition, lookAheadPosition); // Only spawn grass when curve normal is bewteen the allowed angle values if (normalAngle >= minAngle && normalAngle <= maxAngle) { Vector3 spawnPosition = spriteShape.transform.position + currentPosition; Quaternion spawnRotation = Quaternion.AngleAxis(normalAngle - 90f, new Vector3(0f, 0f, 1f)); GameObject grass = Instantiate(GetRandomGrassPrefab(), spawnPosition, spawnRotation, this.transform); grassRandomizer.Randomize(grass); // Randomize grass animation properties } currentT += spacing; t = Mathf.Repeat(currentT, 0.99f); // prevent t overshot to next point } } Debug.Log(string.Format("Population count: {0}", transform.childCount)); } public void Clear() { GameObject[] childs = new GameObject[transform.childCount]; for(int i = 0; i < childs.Length; i++) { childs[i] = transform.GetChild(i).gameObject; } foreach (GameObject child in childs) { DestroyImmediate(child); } } private Vector3 GetPositionInSpline(int startPoint, int endPoint, float t) { Vector3 startPosition = spriteShape.spline.GetPosition(startPoint); Vector3 endPosition = spriteShape.spline.GetPosition(endPoint); // tangents position are relative to the point position Vector3 startTangent = startPosition + spriteShape.spline.GetRightTangent(startPoint); Vector3 endTangent = endPosition + spriteShape.spline.GetLeftTangent(endPoint); return BezierUtility.BezierPoint( startPosition, startTangent, endTangent, endPosition, t); } private float GetNormal(Vector3 from, Vector3 to) { float dx = to.x - from.x; float dy = to.y - from.y; float angle = Mathf.Atan2(dy, dx) * Mathf.Rad2Deg; return angle + 90f; // rotate angle 90 degress to get normal } private GameObject GetRandomGrassPrefab() { return grassPrefabs[Random.Range(0, grassPrefabs.Count)]; } }
#if UNITY_EDITOR using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEditor; [CustomEditor(typeof(GrassPopulator))] public class GrassPopulatorEditor : Editor { public override void OnInspectorGUI() { DrawDefaultInspector(); GrassPopulator populator = (GrassPopulator)target; if (GUILayout.Button("Populate")) { populator.Clear(); populator.Populate(); } if (GUILayout.Button("Clear")) { populator.Clear(); } } } #endif

Crystal patterns

The patterns were created using control points, basically they are points that will be followed by crystals, a pattern looks like the following in edit mode:
In run time the control points are hidden (but are still alive and moving) and the crystals follow the point they are assigned to:
The crystal patterns seen in the game are basically a combination of one or more of the three basic patterns which are the following:
Rotatory pattern
Path pattern
Follow pattern
In order to facilitate the creation of different variations of patterns I wrote a tool where I was able to easily create and experiment with different patterns combinations:

Bonus information

Some additional information for the curious developers:
  • Development time: 28 days
  • Number of commits: 127
Complete view of the scene:
Overview of the game code:
And that's it, I hope you enjoyed taking a look at this little game concept I prepared for the Unity 2D challenge!
Tools used:
  • Unity 2018.2.17f1
  • Unity 2D animation package
  • Unity sprite shape
  • Corgi Engine
  • DOTween Pro
Credits:
Game design, programming and art by Samuel Macedo
Music and sound effects:
  • https://www.soundsnap.com
  • http://www.freesfx.co.uk

Samuel Macedo
Software Engineer - Programmer
1
Comments
Samuel Macedo
a month ago
Software Engineer - Programmer
PaulWell done!
Thank you :)
0
Paul
a month ago
Game Developer - Programmer
Well done!
0
Samuel Macedo
a month ago
Software Engineer - Programmer
Tomas RychnovskyNice explanation of tools!
Thank you very much!
0
Samuel Macedo
a month ago
Software Engineer - Programmer
Saurabh SaxenaNice art & gameplay 🎶💡🖌
Thanks, glad you liked it :)
0
Tomas Rychnovsky
a month ago
Developer - Programmer
Nice explanation of tools!
0