Skip to content

Instantly share code, notes, and snippets.

@yankooliveira
Created March 19, 2018 00:14
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save yankooliveira/cfd690bac53a60fbe79f53ac54b98b03 to your computer and use it in GitHub Desktop.
Save yankooliveira/cfd690bac53a60fbe79f53ac54b98b03 to your computer and use it in GitHub Desktop.
Source code for my #1classjam entry, Chimera
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.PostProcessing;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
using Random = UnityEngine.Random;
public class Chimera : MonoBehaviour
{
public enum EntityType
{
None = 0,
Brick = 1,
Ball = 2,
Paddle = 3,
Game = 4,
Camera = 5,
}
public enum GameState
{
None = 0,
MainMenu = 1,
WonLevel = 2,
LevelPrepare = 3,
InGame = 4,
GameOver = 5,
Glitch = 6,
}
[Header("Entity")]
public EntityType Type;
public float Speed;
public float Width;
public float Height;
public int Health = 1;
public int Points = 1;
public Vector2 MinMaxAngle;
[Header("Game Config")]
public Chimera TemplateBrick;
public Chimera TemplateBall;
public Chimera TemplatePaddle;
public Vector2 MinMaxVerticalBrickAmount = new Vector2(2, 4);
public Vector2 MinMaxHorizontalBrickAmount = new Vector2(2, 7);
public AnimationCurve BrickHealthBalancing;
public AnimationCurve BrickAmountBalancing;
public AnimationCurve BallSpeedBalancing;
public AnimationCurve PlatformSpeedBalancing;
public AnimationCurve ChanceOfGlitch;
[Header("FX")]
public GameObject Particles;
public GameObject WarpEffects;
public float WarpBgDuration = 0.5f;
public PostProcessingProfile BgPostProcessor;
public Material PostProcessMaterial;
public Gradient ColorGradient;
[Header("UI")]
public GameObject StartUI;
public GameObject GameOverUI;
public GameObject LevelUI;
public Text ScoreText;
public Text LivesText;
public Text IncomingLevelText;
public Text FinalScoreText;
[Header("Audio")]
public AudioSource MusicSource;
public AudioSource SFXSource;
public AudioClip SFXGlitch;
public AudioClip SFXBall;
public AudioClip SFXBallDeath;
public AudioClip SFXLevelUp;
public AudioClip[] SFXBreak;
public event Action<Chimera> EntityDied;
public event Action<Chimera> EntityTookDamage;
public event Action<Chimera> EntityMutated;
private EntityType previousType;
public bool HasMutatedOnce
{
get { return previousType != EntityType.None && previousType != Type; }
}
private int brickLayer;
private int defaultLayer;
private int paddleLayer;
private Renderer targetRenderer;
private Vector3 originalLocalPosition;
private void Awake()
{
BuildCaches();
switch (Type)
{
case EntityType.Game: SetupGame(); return;
}
}
private void Update()
{
switch (Type)
{
case EntityType.Ball: UpdateBall(); return;
case EntityType.Paddle: UpdatePaddle(); return;
case EntityType.Camera: UpdateCamera(); return;
case EntityType.Game: UpdateGame(); return;
}
}
private void OnCollisionEnter(Collision collision)
{
switch (Type)
{
case EntityType.Brick: OnCollisionEnterBrick(collision); return;
case EntityType.Ball: OnCollisionEnterBall(collision); return;
}
}
private void BuildCaches()
{
brickLayer = LayerMask.NameToLayer("Brick");
defaultLayer = LayerMask.NameToLayer("Default");
paddleLayer = LayerMask.NameToLayer("Paddle");
originalLocalPosition = transform.localPosition;
targetRenderer = GetComponent<Renderer>();
// Hardcoding this for bricks because I just don't care anymore
if (targetRenderer == null)
{
targetRenderer = transform.GetChild(0).GetComponent<Renderer>();
}
}
private void UpdateMovement()
{
transform.position += currentDirection * Speed * Time.deltaTime;
}
private void Mutate(EntityType type, Material mat = null)
{
if (Type == type) return;
if (!HasMutatedOnce)
{
previousType = Type;
}
Type = type;
switch (type)
{
case EntityType.Ball:
MutateIntoBall();
break;
case EntityType.Brick:
gameObject.layer = brickLayer;
break;
case EntityType.Paddle:
gameObject.layer = paddleLayer;
break;
}
if (mat != null && targetRenderer != null)
{
targetRenderer.material = mat;
}
if (EntityMutated != null)
{
EntityMutated(this);
}
}
private void MutateIntoBall()
{
gameObject.layer = defaultLayer;
currentDirection = Random.insideUnitCircle.normalized;
GetComponent<Collider>().enabled = false;
StartCoroutine(DelayEnableCollider(1f));
}
private IEnumerator DelayEnableCollider(float time)
{
yield return new WaitForSeconds(time);
GetComponent<Collider>().enabled = true;
}
#region Brick
private void SetBrickHealth(int health)
{
if (health > 0)
{
targetRenderer.material.SetColor("_Color", ColorGradient.Evaluate(health / 10f));
}
Health = health;
if (EntityTookDamage != null)
{
EntityTookDamage(this);
}
}
private void OnCollisionEnterBrick(Collision collision)
{
SetBrickHealth(Health - 1);
if (Health <= 0)
{
if (Particles != null)
{
Particles.SetActive(true);
}
GetComponent<Collider>().enabled = false;
transform.GetChild(0).gameObject.SetActive(false);
KillEntity(1f);
}
SFXSource.PlayOneShot(SFXBreak[Random.Range(0, SFXBreak.Length)]);
}
private void AnimateBrickAppear(float delay = 0f)
{
var brickTrans = transform.GetChild(0);
brickTrans.localScale = Vector3.zero;
StartCoroutine(AnimateBrickAppearRoutine(brickTrans, delay));
}
private IEnumerator AnimateBrickAppearRoutine(Transform brickTrans, float delay = 0f)
{
if (delay > 0f)
{
yield return new WaitForSeconds(delay);
}
float timer = 0f;
while (timer < 0.5f)
{
timer += Time.deltaTime;
brickTrans.localScale = Vector3.Lerp(brickTrans.localScale, Vector3.one, timer / 0.5f);
yield return null;
}
}
#endregion
#region Ball
private Chimera ball;
private Vector3 currentDirection;
private void UpdateBall()
{
if (!isLevelRunning)
{
return;
}
UpdateMovement();
var ballViewport = Camera.main.WorldToViewportPoint(transform.position);
bool collided = false;
if (ballViewport.y < 0f)
{
if (isGlitching)
{
currentDirection.y = 1f;
collided = true;
}
else
{
KillEntity();
}
}
else if (ballViewport.y > 1f)
{
currentDirection.y = -1f;
collided = true;
}
if (ballViewport.x < 0f)
{
currentDirection.x = 1f;
collided = true;
}
else if (ballViewport.x > 1f)
{
currentDirection.x = -1f;
collided = true;
}
if (collided)
{
currentDirection.Normalize();
SFXSource.PlayOneShot(SFXBall);
}
}
private void KillEntity(float delay = 0)
{
Destroy(gameObject, delay);
if (EntityDied != null)
{
EntityDied(this);
}
}
private void OnCollisionEnterBall(Collision collision)
{
if (collision.contacts[0].point.x > transform.position.x)
{
currentDirection.x = -1;
}
else if (collision.contacts[0].point.x < transform.position.x)
{
currentDirection.x = 1;
}
if (collision.contacts[0].point.y > transform.position.y)
{
currentDirection.y = -1f;
}
else if (collision.contacts[0].point.y < transform.position.y)
{
currentDirection.y = 1f;
}
if (collision.gameObject.layer.Equals(paddleLayer))
{
var paddle = collision.gameObject.GetComponent<Chimera>();
if (paddle.previousType == EntityType.Brick)
{
paddle.OnCollisionEnterBrick(collision);
}
else
{
currentDirection.y = 1f;
currentDirection.x = paddle.currentDirection.x > 0f ? 1f : -1f;
var factor = Mathf.Abs(paddle.rotateFactor / paddle.MinMaxAngle.x);
if (factor != 0f)
{
currentDirection.x *= factor;
}
}
}
currentDirection.Normalize();
if (!collision.collider.gameObject.layer.Equals(brickLayer))
{
SFXSource.PlayOneShot(SFXBall);
}
}
#endregion
#region Paddle
private Chimera paddle;
private float rotateFactor;
private void UpdatePaddle()
{
if (Input.GetKeyDown(KeyCode.R))
{
SceneManager.LoadScene(0);
}
if (Input.GetKey(KeyCode.LeftArrow) || Input.GetKey(KeyCode.A))
{
currentDirection = Vector3.left;
}
else if (Input.GetKey(KeyCode.RightArrow) || Input.GetKey(KeyCode.D))
{
currentDirection = Vector3.right;
}
else
{
currentDirection = Vector3.zero;
}
UpdateMovement();
float rotateAngle = 0;
if (currentDirection.x != 0f)
{
rotateAngle = currentDirection.x > 0f ? MinMaxAngle.y : MinMaxAngle.x;
}
rotateFactor = Mathf.Lerp(rotateFactor, rotateAngle, Time.deltaTime * 5f);
transform.rotation = Quaternion.AngleAxis(rotateFactor, Vector3.forward);
}
private IEnumerator GlitchRoutine()
{
isGlitching = true;
cameraControl.postprocessorIsEnabled = true;
SFXSource.PlayOneShot(SFXGlitch);
MusicSource.volume = 0f;
yield return new WaitForSeconds(SFXGlitch.length * 0.15f);
cameraControl.postprocessorIsEnabled = false;
isGlitching = false;
yield return new WaitForSeconds(SFXGlitch.length * 0.85f);
MusicSource.volume = 1f;
}
#endregion
#region Game
private GameState currentGameState;
private int currentScore = 0;
private int lives = 3;
private List<Chimera> currentBricks = new List<Chimera>();
private Vector3 bottomLeftBrick = new Vector3(-7f, 5f, 0f);
private Vector3 topRightBrick = new Vector3(7f, 9f, 0f);
private int currentHorizontalBrickAmount;
private int currentVerticalBrickAmount;
private Chimera _cameraControl;
private Chimera cameraControl
{
get
{
if (_cameraControl == null)
{
_cameraControl = Camera.main.GetComponent<Chimera>();
}
return _cameraControl;
}
}
private static bool isLevelRunning;
private static bool isGlitching;
private float nextGlitchTimeLock;
private int ballCount;
private bool isShiningBG = false;
private int currentLevel;
private void SetupGame()
{
WarpEffects = Instantiate(WarpEffects);
SetGameState(GameState.MainMenu);
}
private void PlayerPressedSpace()
{
switch (currentGameState)
{
case GameState.MainMenu:
SetGameState(GameState.LevelPrepare);
break;
case GameState.LevelPrepare:
if (!isLevelRunning)
{
SetGameState(GameState.InGame);
}
break;
case GameState.InGame:
if (!isLevelRunning)
{
isLevelRunning = true;
}
break;
case GameState.GameOver:
SceneManager.LoadScene(0);
break;
}
}
private void SetGameState(GameState state)
{
switch (state)
{
case GameState.MainMenu:
ShowMainMenu(true);
break;
case GameState.LevelPrepare:
ShowMainMenu(false);
ShowLevelPrepare(true);
isLevelRunning = false;
SFXSource.PlayOneShot(SFXLevelUp);
SetupLevel();
ChangeLives(0);
break;
case GameState.InGame:
isLevelRunning = true;
ScoreText.enabled = true;
ShowLevelPrepare(false);
break;
case GameState.GameOver:
ShowEndGame(true);
isLevelRunning = false;
FinalScoreText.text = "Final score: " + currentScore.ToString();
ScoreText.enabled = false;
break;
}
currentGameState = state;
}
private Vector3 GetBrickDimensions()
{
float xSize = topRightBrick.x - bottomLeftBrick.x;
float x = xSize / (float)currentHorizontalBrickAmount;
float ySize = topRightBrick.y - bottomLeftBrick.y;
float y = ySize / (float)currentVerticalBrickAmount;
return new Vector3(x, y, 1f);
}
private void SetupLevel()
{
if (currentLevel == 0)
{
// Don't judge! It's Sunday, I wanna go do something else
Invoke("CreatePaddle", 1f);
Invoke("CreateBall", 1f);
}
else
{
paddle.Speed = PlatformSpeedBalancing.Evaluate(currentLevel);
ball.Speed = BallSpeedBalancing.Evaluate(currentLevel);
}
CreateBricks();
}
private void UpdateGame()
{
if (Input.GetKeyDown(KeyCode.Space))
{
PlayerPressedSpace();
}
if (Input.GetKeyDown(KeyCode.T))
{
MutateRandomBrick();
}
if (Input.GetKeyDown(KeyCode.B))
{
for (int i = currentBricks.Count - 1; i >= 0; i--)
{
currentBricks[i].OnCollisionEnterBrick(null);
}
}
if (Input.GetKeyDown(KeyCode.M))
{
SFXSource.enabled = !SFXSource.enabled;
MusicSource.enabled = !MusicSource.enabled;
}
if (!isLevelRunning && ball != null && paddle != null)
{
ball.transform.position = paddle.transform.position + new Vector3(0f, 1f, 0f);
}
}
private void CheckForGlitches()
{
if (Time.time < nextGlitchTimeLock)
{
return;
}
var chance = ChanceOfGlitch.Evaluate(currentLevel);
if (Random.value < chance && isLevelRunning)
{
MutateRandomBrick();
nextGlitchTimeLock = Time.time + Random.Range(20f, 40f) + 30f * (1 - chance);
}
}
private void MutateRandomBrick()
{
var rnd = Random.Range(0, currentBricks.Count);
var ent = Random.value < 0.5f ? EntityType.Paddle : EntityType.Ball;
currentBricks[rnd].Mutate(ent, ball.targetRenderer.material);
StartCoroutine(GlitchRoutine());
}
private void CreateBricks()
{
float currentLevelFactor = BrickAmountBalancing.Evaluate(currentLevel);
currentVerticalBrickAmount = (int)Mathf.Lerp(MinMaxVerticalBrickAmount.x, MinMaxVerticalBrickAmount.y, currentLevelFactor);
currentHorizontalBrickAmount = (int)Mathf.Lerp(MinMaxHorizontalBrickAmount.x, MinMaxHorizontalBrickAmount.y, currentLevelFactor);
currentBricks.Clear();
Vector3 brickPos = bottomLeftBrick;
float startX = brickPos.x;
Chimera newBrick = null;
Vector3 brickDimensions = GetBrickDimensions();
for (int i = 0; i <= currentVerticalBrickAmount; i++)
{
for (int j = 0; j <= currentHorizontalBrickAmount; j++)
{
newBrick = Instantiate(TemplateBrick);
newBrick.transform.position = brickPos;
newBrick.EntityDied += OnBrickDied;
newBrick.EntityMutated += OnEntityMutated;
newBrick.EntityTookDamage += OnBrickTookDamage;
newBrick.transform.localScale = brickDimensions;
newBrick.Width = brickDimensions.x;
newBrick.Height = brickDimensions.y;
newBrick.AnimateBrickAppear((i + j) * 0.1f);
newBrick.SetBrickHealth(GetBrickHealth());
newBrick.Points = newBrick.Health;
newBrick.SFXSource = SFXSource;
newBrick.SFXBreak = SFXBreak;
currentBricks.Add(newBrick);
brickPos.x += newBrick.Width;
}
brickPos.x = startX;
brickPos.y += newBrick.Height;
}
}
private int GetBrickHealth()
{
float health = 1;
var chance = Random.value * currentLevel / 10f;
if (chance > 0.5f)
{
health = Random.Range(2, BrickHealthBalancing.Evaluate(currentLevel));
}
return Mathf.FloorToInt(health);
}
private void OnBrickTookDamage(Chimera brick)
{
cameraControl.StartShaking(10f, 1f / Mathf.Max(brick.Health, 1f), 0.5f);
if (currentBricks.Count > 1)
{
CheckForGlitches();
}
}
private void OnBrickDied(Chimera brick)
{
currentScore += brick.Points;
ScoreText.text = currentScore.ToString();
cameraControl.BlinkBG();
if (currentScore % 100 == 0)
{
ChangeLives(1);
}
currentBricks.Remove(brick);
if (currentBricks.Count == 0)
{
currentLevel++;
SetGameState(GameState.LevelPrepare);
}
}
private void CreateBall()
{
ball = Instantiate(TemplateBall);
ball.transform.position = new Vector3(0f, 1f);
ball.currentDirection = new Vector3(-1f, -1f);
ball.EntityDied += OnBallDied;
ball.EntityMutated += OnEntityMutated;
ball.SFXSource = SFXSource;
ball.SFXBall = SFXBall;
ball.Speed = BallSpeedBalancing.Evaluate(currentLevel);
ballCount++;
}
private void OnBallDied(Chimera ball)
{
ballCount--;
if (ballCount == 0)
{
WarpBG();
}
SFXSource.PlayOneShot(SFXBallDeath);
ChangeLives(-1);
if (lives <= 0)
{
SetGameState(GameState.GameOver);
}
else
{
CreateBall();
isLevelRunning = false;
}
}
private void ChangeLives(int delta)
{
lives += delta;
if (delta > 0)
{
SFXSource.PlayOneShot(SFXLevelUp);
}
// I'm not even ashamed right now
LivesText.text = Mathf.Pow(10, lives).ToString().Replace("1", "");
}
private void CreatePaddle()
{
paddle = Instantiate(TemplatePaddle);
paddle.transform.position = Vector3.zero;
paddle.transform.localScale = new Vector3(paddle.Width, paddle.Height, 1f);
paddle.Speed = PlatformSpeedBalancing.Evaluate(currentLevel);
paddle.EntityMutated += OnEntityMutated;
}
private void OnEntityMutated(Chimera entity)
{
if (entity.previousType == EntityType.Ball)
{
ballCount--;
}
if (entity.Type == EntityType.Ball)
{
ballCount++;
}
}
private void WarpBG()
{
StartCoroutine(ShineBGRoutine(50f, true));
}
private void BlinkBG()
{
StartCoroutine(ShineBGRoutine(5f, false));
}
private IEnumerator ShineBGRoutine(float intensity, bool warp = false)
{
if (!isShiningBG)
{
isShiningBG = true;
if (warp)
{
WarpEffects.SetActive(true);
}
var bloom = BgPostProcessor.bloom.settings;
float bloomOrigIntensity = bloom.bloom.intensity;
float bloomTargetIntensity = bloom.bloom.intensity = intensity;
float timer = 0f;
BgPostProcessor.bloom.settings = bloom;
while (timer < WarpBgDuration)
{
timer += Time.deltaTime;
bloom.bloom.intensity = Mathf.Lerp(bloomTargetIntensity, bloomOrigIntensity, timer / WarpBgDuration);
BgPostProcessor.bloom.settings = bloom;
yield return null;
}
bloom.bloom.intensity = bloomOrigIntensity;
BgPostProcessor.bloom.settings = bloom;
if (warp)
{
WarpEffects.SetActive(false);
}
isShiningBG = false;
}
}
private void ShowMainMenu(bool show)
{
StartUI.gameObject.SetActive(show);
}
private void ShowLevelPrepare(bool show)
{
if (show)
{
if (currentLevel > 0)
{
LevelUI.SetActive(true);
IncomingLevelText.text = "Level " + (currentLevel + 1).ToString();
}
}
else
{
LevelUI.SetActive(false);
}
}
private void ShowEndGame(bool show)
{
GameOverUI.gameObject.SetActive(show);
}
#endregion
#region Camera
private bool isShaking;
private float lastShakeFrequency;
private float lastShakeMagnitude;
public bool postprocessorIsEnabled;
private void OnRenderImage(RenderTexture source, RenderTexture destination)
{
if (postprocessorIsEnabled)
{
Graphics.Blit(source, destination, PostProcessMaterial);
}
else
{
Graphics.Blit(source, destination);
}
}
private void UpdateCamera()
{
if (isShaking)
{
var shake = PerlinShake(lastShakeFrequency, lastShakeMagnitude);
transform.localPosition = originalLocalPosition + new Vector3(shake.x, shake.y);
}
}
public void StartShaking(float frequency, float magnitude, float duration = -1)
{
isShaking = true;
lastShakeFrequency = frequency;
lastShakeMagnitude = magnitude;
if (duration > 0f)
{
// DON'T JUDGE, IT'S 4:43AM!
Invoke("StopShaking", duration);
}
}
public void StopShaking()
{
isShaking = false;
transform.position = originalLocalPosition;
}
// From http://devblog.aliasinggames.com/camera-shake-earthquake-effect/
public Vector2 PerlinShake(float frequency, float magnitude)
{
Vector2 result;
float seed = Time.time * frequency;
result.x = Mathf.Clamp01(Mathf.PerlinNoise(seed, 0f)) - 0.5f;
result.y = Mathf.Clamp01(Mathf.PerlinNoise(0f, seed)) - 0.5f;
result = result * magnitude;
return result;
}
#endregion
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment