Position
Lead Programmer
Team Size
6 Students
Engine
Unity
Time Frame
3 Months
Release (Internal)
Dec 2019
Platform
Galaxy Tab A
About
Neon is a relaxed platform-puzzle game for the Galaxy Tab A. Players adventure through a color changing synth-wave world where the music and the colors are constantly changing to provide a wide array of emotions from exciting bursts of fun and relaxing wanderings through this neon world. The game is targeted towards players looking for a new-age synth-wave journey that only takes up a commute’s worth of time.
Six Levels
Double Jump, Wall Climb, Dash
Virtual Joystick
Collectables
Multiple Paths
Acceleration Based Player Movement
When developing the movement for Neon, we wanted to have a very satisfying movement curve. Instead of simply setting the velocity to move left and right, I developed a system to set accelerations and deceleration with a maximum velocity. The vertical movement has some similar tweaks including adjusting gravity during different points in the players arc. The player also has some extra control over the jump on the height similar to Mario games.
Virtual Joystick with programmable dead zones
Highly ability to modify acceleration and deceleration values
Variable height jump based on player input
Focused on movement feeling
// Update is called once per frame
public void UpdateJoystick()
{
if( Input.touchCount > 0 && m_IsJoystickActive )
{
foreach(Touch t in Input.touches)
{
JoystickCalculation(t);
}
}
if(Input.touchCount == 0)
{
m_IsJoystickActive = false;
m_Magnitude = 0f;
m_MagnitudeNoCurve = 0f;
m_JoystickPosition = Vector2.zero;
m_Angle = 0;
}
}
// Determines if the touch should be calculated
private void JoystickCalculation(Touch touch)
{
if(touch.fingerId != m_JoystickFingerId) { return; }
Vector2 joystickDisplacement = Vector2.ClampMagnitude(touch.position - m_JoystickCenter, m_MaximumJoystickDistance);
DeadzoneCorrection(joystickDisplacement);
}
// Deadzone correction
private void DeadzoneCorrection(Vector2 rangeToMax)
{
float pureRadius = rangeToMax.magnitude * 1f / m_MaximumJoystickDistance;
float correctedRadius = pureRadius.Remap(m_InnerDeadzone, m_OuterDeadzone, 0f, 1f).Clamp(0f, 1f);
if( correctedRadius > 0)
{
m_Angle = Mathf.Rad2Deg * Mathf.Atan2(rangeToMax.y, rangeToMax.x);
if(m_Angle < 0)
{
m_Angle = 360 + m_Angle;
}
m_MagnitudeNoCurve = correctedRadius;
// Magnitude Curve
if(correctedRadius < .333f)
{
m_Magnitude = .333f;
}
else if (correctedRadius < .666f)
{
m_Magnitude = .666f;
}
else if(correctedRadius < 1f)
{
m_Magnitude = 1f;
}
m_Magnitude = correctedRadius;
m_JoystickPosition = new Vector2(m_Magnitude * Mathf.Cos(m_Angle * Mathf.Deg2Rad),
m_Magnitude * Mathf.Sin(m_Angle * Mathf.Deg2Rad));
}
else
{
m_Magnitude = 0f;
m_MagnitudeNoCurve = 0f;
m_JoystickPosition = Vector2.zero;
m_Angle = 0f;
}
}
Neon Shader Effect
One of the major challenges with Neon was the visuals and how to get quality neon glow effect on the relatively low specs of our devices. I spent quite a long time investigating alternatives but eventually decided that I needed a custom solution. I started with a simple blur effect and inserted it into the middle of Unity’s render pipeline. I made sure to only blur the objects we wanted the effect on. Finally, I combined the scene with the blurred texture such that the resulting alpha follows a similar function: f(x) = -|x| + 1 centered around the edges of the objects.
Custom blur shader for the glowing objects
Can modify the shader from Unity Editor
Performant on low device specs
Adds unique look to the environment
// Customization from Unity Engine
#if _SAMPLES_LOW
#define SAMPLES 20
#elif _SAMPLES_MEDIUM
#define SAMPLES 50
#elif _SAMPLES_HIGH
#define SAMPLES 100
#endif
// Horizontal Blur Pass
fixed4 frag( v2f i ): SV_Target
{
fixed4 col = 0;
// Get 10 averages on the vertical
for ( float index = 0; index < SAMPLES; ++index )
{
float2 uv = i.uv + float2(0, (index / (SAMPLES - 1) - .5) * _BlurSize);
col += tex2D( _MainTex, uv );
}
// Average the colors
col = col / SAMPLES;
return col;
}
// Vertical Blur Pass
fixed4 frag( v2f i ): SV_Target
{
float inverseAspect = _ScreenParams.y / _ScreenParams.x;
fixed4 col = 0;
// Get 10 averages on the vertical
for ( float index = 0; index < SAMPLES; ++index )
{
float2 uv = i.uv + float2((index / (SAMPLES - 1) - 0.5) * _BlurSize * inverseAspect, 0);
col += tex2D( _MainTex, uv );
}
// Average the colors
col = col / SAMPLES;
return col;
}
// Addative result (Scene + Blur Texture)
sampler2D _Background;
sampler2D _Additive;
float _AlphaAdditiveCutoff;
fixed4 frag( v2f i ): SV_Target
{
fixed4 colBack = tex2D( _Background, i.uv );
fixed4 colAdd = tex2D( _Additive, i.uv );
if ( _AlphaAdditiveCutoff < colAdd.a )
{
return colBack + colAdd * (1 - colAdd.a);
}
return colBack + colAdd;
}
Saved Games
During development, once the collectables were implemented we realized that we needed a way to save and load the players progression through play sessions. To achieve that, I used Newtonsoft to serialize and de-serialize the list of level progressions to store on device. This allowed the player to exit the game and save their current progress. Whenever the game was paused in android, I would quickly write out the current data to the device. When the game was loaded, I would query that file. If the file was gone, I would create a new list of level states that would be used for the remainder of the game, being saved back to disk when the game exited.
Persistent progression data
Saved and loaded based on application state
Used third party serialization library
// Initialize the game
public void Initialize()
{
AudioManager.Instance.SetupMixer();
AudioManager.Instance.SetupSounds();
gameObject.tag = "GameController";
m_IsInitialized = true;
bool noInitilization = true;
// Load the data from disc
m_LevelStoragePath = Application.persistentDataPath + "/LevelData.json";
if (File.Exists(m_LevelStoragePath))
{
UnityEngine.Debug.Log("Loading file from data");
StreamReader fileReader = new StreamReader(m_LevelStoragePath);
m_LevelDatas = JsonConvert.DeserializeObject<List<LevelData>>(fileReader.ReadToEnd());
if (m_LevelDatas != null)
{
noInitilization = false;
}
else
{
UnityEngine.Debug.LogError("ERROR: Data failed to load");
}
fileReader.Close();
}
// Verify that the file loaded
if (noInitilization)
{
UnityEngine.Debug.Log("No file Found/Failed to initialize");
m_LevelDatas = new List<LevelData>();
for (int i = 0; i < NumberOfLevels; i++)
{
m_LevelDatas.Add(new LevelData(i));
}
}
}
// Called when the application is minimized
private void OnApplicationPause()
{
UnityEngine.Debug.Log("Writing file to disc");
UnityEngine.Debug.Log("Storing data at " + m_LevelStoragePath);
StreamWriter fileWriter = new StreamWriter(m_LevelStoragePath, false);
string temp = JsonConvert.SerializeObject(m_LevelDatas);
fileWriter.Write(temp);
fileWriter.Close();
}
Production and Scrum
Of the team members originally on our team, I was that one that had the most professional experience with the scrum process and software development. During the early sprints, I helped guide our team through and answer any questions. However, as the team became more confident, I became more hands off. We chose to swap scrum masters every sprint to allow each of our team members the opportunity. During sprint planning, I really tried to drive the other team members to very granular tasks and helped them break up tasks that would be too large for our specific timeline. As the team improved, they became more self-reliant, including taking on some very major production tasks without being required of them.
Scrum development cycle
Sprint planning and retros
Rotating scrum master
Retrospective
What Went Well
Team composition worked well from the beginning
Cross discipline communication - the moment a asset was ready, we would announce to the awaiting discipline its completion
Very fine task breakdowns
What Went Wrong
Task estimation needed improvement going through the sprints
Under estimated time/hours available during a sprint (alpha sprint)
Used sprint time for meetings that were not allocated/exceeded time box
Even Better Ifs
Schedule additional time for meetings and guaranteed tasks such as bug fixing
Schedule weekly meetings to refine game design and discuss the state of the game