Platform
Engine
Language
Development Time
Team Size
PC
Unity
C#
7 weeks
4 design + 5 art
Ygg is a third-person story-based adventure game with focus on visual narrative and experimental mechanics. In Ygg the player uses a flute to solve puzzles and interact with the environment to gain information about the world and protagonist.
As a programmer, the most notable points I was responsible for were:
The camera system
The puzzle system
As well as other miscellaneous tasks such as implementing audio, small interactions with the
environment and bug-fixing.
The camera controller is highly customizable with a high number of systems interacting together to create a smooth experience.
Determines how much the player can pitch the camera up and down, as well as how fast (speed also determines yaw input speed)
As the player looks upward, the camera will move closer to the player. The curve determines how the distance interterpolates between lowest to highest pitch.
Same as with distance. The reason the curve is flipped is because the FoV should be higher when looking upward.
When pitching upwards, the camera offsets a bit to pivot above the player, so that the player can see more clearly (and also avoid the camera going below ground).
As the player walks, the camera should 'drift' to always look in the direction the player is moving. This process is rather complicated with a lot of timers and curves, which is the result of playtesting and iteration.
using UnityEngine; using System.Collections; public struct CameraView { public float pitch, yaw; public float h_offset, v_offset; public float distance; public float fieldOfView; public Vector3 pivot; public Vector3 Forward { get { return new Vector3(Mathf.Sin(yaw * Mathf.Deg2Rad), 0f, Mathf.Cos(yaw * Mathf.Deg2Rad)); } } public Vector3 Right { get { return new Vector3(Mathf.Cos(yaw * Mathf.Deg2Rad), 0f, -Mathf.Sin(yaw * Mathf.Deg2Rad)); } } public Vector3 Offset { get { return Right * h_offset + Vector3.up * v_offset; } } public Vector3 PivotOffset { get { return pivot + Offset; } } public Vector3 World { get { return PivotOffset + Rotation * new Vector3(0f, 0f, -1f) * distance; } } public Quaternion Rotation { get { return Quaternion.Euler(pitch, yaw, 0f); } } public CameraView(float pitch, float yaw, float h_offset, float v_offset, Vector3 pivot, float distance, float fieldOfView) { this.pitch = pitch; this.yaw = yaw; this.h_offset = h_offset; this.v_offset = v_offset; this.pivot = pivot; this.distance = distance; this.fieldOfView = fieldOfView; } public static CameraView Lerp(CameraView a, CameraView b, float f) { CameraView c = a + (b - a) * Mathf.Clamp(f, 0f, 1f); c.pitch = Mathf.LerpAngle(a.pitch, b.pitch, f); c.yaw = Mathf.LerpAngle(a.yaw, b.yaw, f); return c; } public static CameraView operator +(CameraView a, CameraView b) { a.pitch += b.pitch; a.yaw += b.yaw; a.h_offset += b.h_offset; a.v_offset += b.v_offset; a.pivot += b.pivot; a.distance += b.distance; a.fieldOfView += b.fieldOfView; return a; } public static CameraView operator -(CameraView a, CameraView b) { return a + (b * -1f); } public static CameraView operator *(CameraView c, float f) { c.pitch *= f; c.yaw *= f; c.h_offset *= f; c.v_offset *= f; c.pivot *= f; c.distance *= f; c.fieldOfView *= f; return c; } public static CameraView operator /(CameraView c, float f) { //NaN if (f == 0f) return c; return c * (1f / f); } }
using UnityEngine; using System.Collections; public class CameraController : MonoBehaviour { Player m_player; public Player player { get { return m_player; } private set { m_player = value; } } Camera m_camera; [HideInInspector] public CameraView m_localView; [HideInInspector] public CameraView m_localView_Target; //Pitching the camera [Header("Input")] [Tooltip("Pitch clamping")] [SerializeField] Range m_pitchRange = new Range(-40f, 50f); float targetPitchValue { get { return m_pitchRange.LinearValue(m_localView_Target.pitch); } } //Invert camera? bool invertVertical { get { return OptionsManager.active.invertCamera; } } //Look speed [SerializeField] float m_inputSpeed = 120f; //Distance [Header("Distance")] [SerializeField] Range m_distanceRange = new Range(5f, 30f); [SerializeField] AnimationCurve m_distanceCurve = null; //View lerp speed [SerializeField] float m_lerpSpeed = 5f; [Header("Field Of View")] //Field of view [SerializeField] Range m_fovRange = new Range(90f, 60f); [SerializeField] AnimationCurve m_fovCurve = null; [Header("Offseting")] //Distance position offset [SerializeField] Vector2 m_zoomOffset = new Vector2(0f, 0f); [SerializeField] AnimationCurve m_zoomOffsetCurve = null; [SerializeField] float m_velocityOffsetFactor = 0.1f; //Drift speed float m_driftRaw = 1f; //Raw drift value [Header("Drifting")] [SerializeField] AnimationCurve m_driftCurve = null; //Easing of the drifting [SerializeField] Timer m_driftTimer = new Timer(3f, 1f); //Delay for drifting [SerializeField] Range m_driftSpeedRange = new Range(0.5f, 2f); //Drifting speed, from full control to least [SerializeField] AnimationCurve m_driftSpeedCurve = null; //Drifting speed curve [SerializeField] float m_driftIncreaseIdle = 0.4f; [SerializeField] float m_driftIncreaseWalk = 1f; [SerializeField] float m_driftDecrease = 1f; [Header("Drifting (Walking)")] [SerializeField] float m_walkDriftPitch = 40f; [SerializeField] float m_slopeUphillInfluence = 0.7f; [SerializeField] float m_slopeDownhillInfluence = 0.4f; [Tooltip("Amount of drifting of the angle compared to the camera (0-180 degrees)")] [SerializeField] AnimationCurve m_walkAngleDriftCurve = null; //Drifting speed curve for walking angle (0 - 180) float drift { get { return m_driftCurve.Evaluate(m_driftRaw); } } float driftSpeed { get { return m_driftSpeedRange.Lerp(m_driftSpeedCurve.Evaluate(drift)); } } bool inputOverride { get { return m_hintCollection.InputOverride || (m_driftVolume != null && m_driftVolume.inputOverride); } } [HideInInspector] public float m_globalOffsetH = 0f; [HideInInspector] public float m_globalOffsetV = 0f; //Walking vertical angle float slopeAngle { get { float angle = 0f; Vector3 playerPos = m_player.transform.position; Vector3 offset = playerPos + m_player.transform.rotation * new Vector3(0f, 0f, 3f); RaycastHit hit; if (Physics.Raycast(offset + Vector3.up * 3f, Vector3.down, out hit, 6f)) { Vector3 hitPoint = hit.point; angle = Mathf.Atan2(hitPoint.y - playerPos.y, 3f) * Mathf.Rad2Deg; } return angle; } } //Hint collection [HideInInspector] public CameraHintCollection m_hintCollection = new CameraHintCollection(); //Drift volume [HideInInspector] public DriftVolume m_driftVolume = null; //Collision checking float m_collisionInfluence = 0f; float m_collisionDistance = 0f; //View with cameravolumes included CameraView targetView { get { CameraView hintView = m_hintCollection.View; return CameraView.Lerp(m_localView_Target, hintView, m_hintCollection.Weight * drift); } } void Start() { //Find player m_player = GetComponentInParent<Player>(); m_localView.yaw = m_localView_Target.yaw = m_player.transform.rotation.eulerAngles.y; m_localView.pivot = m_localView_Target.pivot = m_player.transform.position; m_localView.distance = m_localView_Target.distance = 20f; transform.parent = null; //Find camera m_camera = GetComponent<Camera>(); } void Update() { if (m_player.inputEnabled) UpdateInput(); UpdateDrift(); UpdateTarget(); UpdateCollision(); UpdateLocal(); UpdateCamera(); } // //UPDATE TARGET // void UpdateTarget() { m_localView_Target.pivot = m_player.transform.position + new Vector3(0f, 1f, 0f) + m_player.m_movement.velocity * m_velocityOffsetFactor; m_localView_Target.distance = m_distanceRange.Lerp(m_distanceCurve.Evaluate(targetPitchValue)); m_localView_Target.fieldOfView = m_fovRange.Lerp(m_fovCurve.Evaluate(targetPitchValue)); Vector2 offset = m_zoomOffset * m_zoomOffsetCurve.Evaluate(targetPitchValue); m_localView_Target.h_offset = offset.x; m_localView_Target.v_offset = offset.y; } // //INPUT // void UpdateInput() { if (inputOverride || m_player.playingFlute) return; Vector2 input = new Vector2(Input.GetAxisRaw("RightHorizontal"), Input.GetAxisRaw("RightVertical")); if (input.magnitude > 0.1f) { if (invertVertical) input.y *= -1; m_localView_Target.yaw += input.x * m_inputSpeed * Time.deltaTime; m_localView_Target.pitch += input.y * m_inputSpeed * Time.deltaTime; m_localView_Target.pitch = m_pitchRange.Clamp(m_localView_Target.pitch); m_driftRaw = Mathf.Clamp(m_driftRaw - m_driftDecrease * input.magnitude * Time.deltaTime, 0f, 1f); m_driftTimer.Reset(); } } // //DRIFT // void UpdateDrift() { Vector2 playerVelocity = m_player.m_movement.movementVelocity; float playerSpeed = playerVelocity.magnitude / m_player.m_movement.maxSpeed; m_driftRaw = Mathf.Clamp(m_driftRaw + (m_driftIncreaseIdle + m_driftIncreaseWalk * playerSpeed) * m_driftTimer.Value * Time.deltaTime, 0f, 1f); m_driftTimer.Update(); Drift(m_hintCollection.Yaw, m_hintCollection.Pitch, m_hintCollection.Weight); if (m_driftVolume) Drift(m_driftVolume.yaw, m_driftVolume.pitch, drift * m_driftVolume.weight); if (playerSpeed > 0.1f) { float angle = Mathf.Atan2(playerVelocity.x, playerVelocity.y) * Mathf.Rad2Deg; float angleFactor = m_walkAngleDriftCurve.Evaluate(Mathf.Abs(Mathf.DeltaAngle(targetView.yaw, angle) / 180f)); float slopePitch = slopeAngle; slopePitch *= slopePitch >= 0f ? m_slopeUphillInfluence : m_slopeDownhillInfluence; Drift(angle, m_walkDriftPitch - slopePitch, (1f - m_hintCollection.Weight) * angleFactor * playerSpeed); } } void Drift(float yaw, float pitch, float factor = 1f) { m_localView_Target.yaw = Mathf.LerpAngle(m_localView_Target.yaw, yaw, driftSpeed * factor * Time.deltaTime); m_localView_Target.pitch = Mathf.LerpAngle(m_localView_Target.pitch, pitch, driftSpeed * factor * Time.deltaTime); } void UpdateCollision() { Vector3 startPosition = m_localView_Target.pivot; Vector3 targetPosition = m_localView_Target.World; RaycastHit hit; if (Physics.SphereCast(startPosition, 1f, targetPosition - startPosition, out hit, m_localView_Target.distance, LayerMask.GetMask("CameraBlocking"))) { m_collisionInfluence = Mathf.Lerp(m_collisionInfluence, 1f, 5f * Time.deltaTime); m_collisionDistance = Mathf.Lerp(m_collisionDistance, hit.distance, 5f * Time.deltaTime); m_localView_Target.distance = Mathf.Lerp(m_localView_Target.distance, m_collisionDistance, m_collisionInfluence); } else { m_collisionInfluence -= 0.5f * Time.deltaTime; m_collisionDistance = Mathf.Lerp(m_collisionDistance, m_localView_Target.distance, 5f * Time.deltaTime); } } // //LOCAL // void UpdateLocal() { m_localView = CameraView.Lerp(m_localView, targetView, (m_lerpSpeed + 5f * (1f - m_distanceRange.LinearValue(m_localView_Target.distance))) * Time.deltaTime); } // //CAMERA // void UpdateCamera() { m_camera.transform.position = m_localView.World; //Add global offset m_camera.transform.position += m_camera.transform.right * m_globalOffsetH + m_camera.transform.up * m_globalOffsetV; m_camera.transform.rotation = m_localView.Rotation; m_camera.fieldOfView = m_localView.fieldOfView; } public void VolumeEnter(CameraVolume volume) { m_hintCollection.Enter(volume); } public void VolumeEnter(DriftVolume volume) { m_driftVolume = volume; } public void VolumeExit(CameraVolume volume) { m_hintCollection.Exit(volume); } public void VolumeExit(DriftVolume volume) { m_driftVolume = null; } }
using UnityEngine; using System.Collections; public class CameraVolume : MonoBehaviour { public virtual float weight { get { return 1f; } } public virtual Vector3 position { get { return m_target.transform.position; } } public virtual Quaternion rotation { get { return m_target.transform.rotation; } } public virtual float yaw { get { return rotation.eulerAngles.y; } } public virtual float pitch { get { return rotation.eulerAngles.x; } } public virtual float fieldOfView { get { return m_target.fieldOfView; } } public virtual bool inputOverride { get { return weight >= m_inputOverrideWeight ? m_inputOverride : false; } } public virtual CameraView orientation { get { return new CameraView(pitch, yaw, 0f, 0f, position, 0f, fieldOfView); } } [SerializeField] Camera m_target; [SerializeField] bool m_inputOverride = false; [SerializeField] float m_inputOverrideWeight = 0.4f; [SerializeField] bool m_active = true; [SerializeField] bool m_onlyOnce = false; public bool active { get { return m_active; } set { m_active = value; } } protected Player m_player = null; public virtual void PlayerEnter(Player player) { if (!m_active) return; m_player = player; if (m_onlyOnce) active = false; } public virtual void PlayerExit(Player player) { m_player = null; } }
To guide the players' eyes on certain key-points we can place focus volumes.
When the player enters the focus volume, the camera will interpolate towards a specific orientation and stay there until the player leaves or changes the camera angle manually.
using UnityEngine; using System.Collections; public class CameraVolume_Focus : CameraVolume { float m_weight; public override float weight { get { return m_weight; } } float currentWeight { get { if (!m_player) return 0f; Vector3 scale = transform.lossyScale; Vector3 scaledPosition = (m_player.transform.position - transform.position) / scale.z; return m_weightCurve.Evaluate(1f - scaledPosition.magnitude / GetComponent<SphereCollider>().radius); } } [SerializeField] AnimationCurve m_weightCurve = null; void Update() { m_weight = Mathf.Lerp(m_weight, currentWeight, 1.5f * Time.deltaTime); } }
During long walks we can place rail volumes to guide the player in a certain direction or have them look at a specific object.
The rail camera works by placing 2 or more camera points, which the volume will then interpolate between as the player moves through.
using UnityEngine; using System.Collections; public class CameraVolume_Rail : CameraVolume { public override Vector3 position { get { return cameraPosition; } } public override Quaternion rotation { get { return cameraRotation; } } float m_currentWeight = 0f; public override float weight { get { return m_currentWeight; } } float targetWeight { get { if (!m_player) return 0f; return Mathf.Min(m_weightCurve.Evaluate(railValue), m_weightTangentCurve.Evaluate(railTangentValue)); } } public override float fieldOfView { get { return Mathf.Lerp(m_cameras[0].fieldOfView, m_cameras[1].fieldOfView, weight); } } float m_railValue = 0f; float railValue { get { if (m_player) m_railValue = (Vector3.Dot(m_player.transform.position - transform.position, m_railDirection) / (volumeSize.z / 2) + 1f) / 2f; return m_railValue; } } float m_railTangentValue = 0f; float railTangentValue { get { if (m_player) m_railTangentValue = 1f - Mathf.Abs( Vector3.Dot(m_player.transform.position - transform.position, m_railTangentDirection) / (volumeSize.x / 2) ); return m_railTangentValue; } } Vector3 cameraPosition { get { Vector3 position = m_cameras[0].transform.position; int transitionCount = m_cameras.Length - 1; float value = railValue; for (int i = 0; i < transitionCount; i++) { float w = Mathf.Clamp(value * transitionCount - i, 0f, 1f); position = Vector3.Lerp(position, m_cameras[i+1].transform.position, w); } return position; } } Quaternion cameraRotation { get { Quaternion rotation = m_cameras[0].transform.rotation; int transitionCount = m_cameras.Length - 1; float value = railValue; for (int i = 0; i < transitionCount; i++) { float w = Mathf.Clamp(value * transitionCount - i, 0f, 1f); rotation = Quaternion.Lerp(rotation, m_cameras[i+1].transform.rotation, w); } return rotation; } } Vector3 volumeSize { get { return Vector3.Scale(GetComponent<BoxCollider>().size, transform.lossyScale); } } [SerializeField] AnimationCurve m_weightCurve = null; [SerializeField] AnimationCurve m_weightTangentCurve = null; [SerializeField] Camera[] m_cameras = null; Vector3 m_railDirection; Vector3 m_railTangentDirection; void Start() { m_railDirection = transform.forward; m_railTangentDirection = transform.right; } void Update() { m_currentWeight = Mathf.Lerp(m_currentWeight, targetWeight, 1.2f * Time.deltaTime); } }
When the player encounters a token like this, it's time to solve a puzzle.
The puzzles takes the shape of melodies that the player has to decode and play using the flute.
using UnityEngine; public struct Note { public const int Count = 4; public enum Type { Right = 0, Up = 1, Left = 2, Down = 3, None = -1 }; public static Color[] Colors = { new Color(1f, 0f, 0f), new Color(0f, 1f, 0f), new Color(0f, 0f, 1f), new Color(1f, 0f, 1f) }; public static Note FromGrip(Vector2 grip) { if (grip.magnitude < 0.8f) return new Note(Type.None); float gripAngle = PolarUtils.Mod(Mathf.Atan2(grip.y, grip.x) * Mathf.Rad2Deg, 0f, 360f); float angle = 360f / Count; for (int i = 0; i < Count; i++) { float a = gripAngle - angle * i; if (a > 180f) a -= 360f; if (a >= -20f && a <= 20f) return new Note((Type)i); } return new Note(Type.None); } public static Note none { get { return new Note(Type.None); } } Type m_type; public Type type { get { return m_type; } } public Color color { get { //Default gray color if (type == Type.None) return new Color(0.4f, 0.4f, 0.4f); //Else, get pretty one from the list :) return Colors[(int)m_type]; } } public Note(Type type) { m_type = type; } public Note(int type) { m_type = (Type)Mathf.Clamp(type, 0, Count); } public override bool Equals(object obj) { if (obj == null) return false; return ((Note)obj).m_type == m_type; } public override int GetHashCode() { return (int)m_type; } public static implicit operator bool(Note n) { return n.type != Type.None; } public static explicit operator int(Note n) { return (int)n.type; } public static bool operator !=(Note a, Note b) { return !(a == b); } public static bool operator ==(Note a, Note b) { return a.Equals(b); } public override string ToString() { return type.ToString(); } }
using UnityEngine; using System.Collections; using System.Collections.Generic; [System.Serializable] public class MelodyNote { [SerializeField] Note.Type m_type; [SerializeField] int m_order; public Note.Type type { get { return m_type; } } public int order { get { return m_order; } set { m_order = value; } } Timer holdTimer = new Timer(0.4f); public MelodyNote(Note.Type type, int order = 0) { m_type = type; m_order = order; } public float Hold() { holdTimer.Update(); return holdTimer.Value; } public void Release() { holdTimer.Reset(); } public bool Evaluate(Note.Type type) { return Evaluate(new Note(type)); } public bool Evaluate(Note n) { return n.type == m_type; } public bool Evaluate(Note.Type type, int order) { return Evaluate(new Note(type), order); } public bool Evaluate(Note n, int order) { return n.type == m_type && (m_order == 0 || m_order == order); } } public class Melody : MonoBehaviour { public enum PlayResult { Hold, NoteDone, MelodyDone, Fail, None } public class MelodyIndicatorList : IEnumerable { List<MelodyIndicator>[] indicatorLists = new List<MelodyIndicator>[4]; public MelodyIndicatorList() { for (int i = 0; i < indicatorLists.Length; i++) indicatorLists[i] = new List<MelodyIndicator>(); } public void Add(MelodyIndicator indicator) { indicatorLists[(int)indicator.type].Add(indicator); } public IEnumerable this[int index] { get { foreach (MelodyIndicator i in indicatorLists[index]) yield return i; } } public IEnumerable this[MelodyNote note] { get { int index = (int)note.type; foreach (MelodyIndicator i in this[index]) yield return i; } } IEnumerator IEnumerable.GetEnumerator() { for (int i = 0; i < 4; i++) foreach (MelodyIndicator indicator in this[i]) yield return indicator; } } //Notes List<MelodyNote> m_noteList = new List<MelodyNote>(); List<MelodyNote> m_noteBuffer = new List<MelodyNote>(); List<MelodyNote> m_playBuffer = new List<MelodyNote>(); //Indicators MelodyIndicatorList m_indicators = new MelodyIndicatorList(); //Playing stuff List<MelodyNote> allCandidates { get { List<MelodyNote> notes = new List<MelodyNote>(); foreach (MelodyNote n in m_noteBuffer) if (n.order == 0 || n.order == currentOrder) notes.Add(n); return notes; } } List<MelodyNote> orderCandidates { get { List<MelodyNote> notes = new List<MelodyNote>(); foreach (MelodyNote n in m_noteBuffer) if (n.order != 0 && n.order == currentOrder) notes.Add(n); return notes; } } public bool finished { get { return m_noteBuffer.Count == 0; } } int currentOrder { get { return m_noteList.Count - m_noteBuffer.Count + 1; } } bool m_active = true; public bool active { get { return m_active; } set { m_active = value; } } //--------------- void Awake() { FindIndicators(); } void Start() { Reset(); } void FindIndicators() { foreach (MelodyIndicator indicator in GetComponentsInChildren<MelodyIndicator>()) { MelodyNote note = FindNote(indicator.type); if (note == null) { note = new MelodyNote(indicator.type); m_noteList.Add(note); } if (indicator.instruction == MelodyIndicator.Instruction.Order) note.order++; m_indicators.Add(indicator); indicator.m_melody = this; } } MelodyNote FindNoteBuffer(Note.Type type) { return m_noteBuffer.Find((x) => x.type == type); } MelodyNote FindNote(Note.Type type) { return FindNote(new Note(type)); } MelodyNote FindNote(Note note) { return m_noteList.Find((x) => x.Evaluate(note)); } public void Reset() { m_noteBuffer.Clear(); m_noteBuffer.AddRange(m_noteList); foreach (MelodyIndicator i in m_indicators) i.Reset(); m_playBuffer.Clear(); foreach (MelodyNote n in m_noteList) n.Release(); } public void Release(Note n) { Release(FindNote(n)); } void Release(MelodyNote n) { if (n == null) return; n.Release(); m_playBuffer.Remove(n); foreach (MelodyIndicator m in m_indicators[n]) m.progress = 0f; } public PlayResult Play(Note n, bool holding) { if (!active) return PlayResult.None; MelodyNote note = FindNoteBuffer(n.type); //Note doesn't exist in buffer if (note == null) { //If not holding, the player actively chose the wrong one //Punish! if (!holding) { OnFail(); return PlayResult.Fail; } //Otherwise, be more lenient :) else return PlayResult.None; } //Is this note note in the playbuffer? if (!m_playBuffer.Contains(note)) { //If not holding, add to the buffer if (!holding) m_playBuffer.Add(note); //Otherwise return else return PlayResult.None; } //Hold this note float progress = note.Hold(); foreach (MelodyIndicator i in m_indicators[note]) i.progress = progress; //Timer done, proc this note! if (progress >= 1f) return OnPlay(note); //Otherwise, nothing else return PlayResult.Hold; } PlayResult OnPlay(MelodyNote note) { //Order pool is empty, or contains note List<MelodyNote> oCandidates = orderCandidates; if (allCandidates.Contains(note) && //Is in candidate pool (oCandidates.Count == 0 || oCandidates.Contains(note))) //Order candidates is empty, or contains note { m_noteBuffer.Remove(note); if (finished) { OnMelodyDone(note); return PlayResult.MelodyDone; } else { OnNoteDone(note); return PlayResult.NoteDone; } } else { OnFail(); return PlayResult.Fail; } } public void OnNoteDone(MelodyNote note) { Release(note); foreach (MelodyIndicator i in m_indicators[note]) i.Done(); } public void OnMelodyDone(MelodyNote note) { Release(note); foreach (MelodyIndicator i in m_indicators[note]) i.Done(); foreach (MelodyIndicator i in m_indicators) i.MelodyFinished(); } public void Fail() { OnFail(); } void OnFail() { Reset(); foreach (MelodyIndicator i in m_indicators) i.Fail(); } }
The player will encounter beings called Wisps while playing.
Their behaviour is semi-random and controllable by walking into certain collision volumes.
using UnityEngine; using System.Collections; using System; public class Wisp : MonoBehaviour { public struct Rotation { public static Rotation identity { get { return new Rotation(0f, 0f); } } float m_pitch, m_yaw; public float yaw { get { return m_yaw; } set { m_yaw = value; } } public float pitch { get { return m_pitch; } set { m_pitch = value; } } public Quaternion quaternion { get { return Quaternion.Euler(m_pitch, m_yaw, 0f); } } public Rotation(Vector3 forward) { forward.Normalize(); m_yaw = Mathf.Atan2(forward.x, forward.z) * Mathf.Rad2Deg; m_pitch = -Mathf.Atan2(forward.y, 1f - Mathf.Abs(forward.y)) * Mathf.Rad2Deg; } public Rotation(float yaw, float pitch) { m_yaw = yaw; m_pitch = pitch; } public static Rotation Lerp(Rotation a, Rotation b, float t) { return new Rotation(Mathf.LerpAngle(a.m_yaw, b.m_yaw, t), Mathf.LerpAngle(a.m_pitch, b.m_pitch, t)); } public static Rotation Rotate(Rotation a, Rotation b, float max) { return new Rotation(Mathf.MoveTowardsAngle(a.m_yaw, b.m_yaw, max), Mathf.MoveTowardsAngle(a.m_pitch, b.m_pitch, max)); } public static Vector3 operator *(Rotation r, Vector3 v) { return r.quaternion * v; } public static Rotation operator *(Rotation r, float f) { return new Rotation(r.yaw * f, r.pitch * f); } public static Rotation operator +(Rotation a, Rotation b) { return new Rotation(a.yaw + b.yaw, a.pitch + b.pitch); } } [HideInInspector] public Vector3 m_velocity = Vector3.zero; Rotation m_rotation = Rotation.identity; Rotation m_rotationTarget = Rotation.identity; public Rotation rotation { get { return m_rotation; } } public float pitchValue { get { return (m_rotation.pitch + 90f) / 180f; } } [Header("Physics")] [SerializeField] float m_friction = 1.2f; [SerializeField] float m_acceleration = 0.4f; [SerializeField] float m_rotationLerp = 1.5f; [SerializeField] float m_rotationSpeed = 5f; [SerializeField] Range m_accelerationRange = new Range(0.7f, 2f); [Header("Rolling")] [SerializeField] float m_rollFactor = 0.8f; [SerializeField] float m_rollMax = 42f; float m_roll = 0f; float m_rollTarget = 0f; WispDriftController m_driftController; Rotation drift { get { return m_driftController.drift; } } float driftAmount { get { return currentBehaviour.driftAmount; } } [Header("Behaviours")] [SerializeField] WispBehaviour[] m_behaviourList; int m_behaviourIndex = 0; WispBehaviour m_behaviourOverride; public WispBehaviour currentBehaviour { get { return m_behaviourOverride ? m_behaviourOverride : m_behaviourList[m_behaviourIndex]; } } [Header("Collision Entering")] [SerializeField] GameObject m_collisionParticle = null; Timer m_idleTimer = new Timer(8f); void Start() { m_driftController = GetComponent<WispDriftController>(); m_rotation = m_rotationTarget = new Rotation(transform.forward); if (m_behaviourList.Length == 0) m_behaviourList = GetComponents<WispBehaviour>(); } public void Turn(float yaw, float pitch) { Turn(new Rotation(yaw, pitch)); } public void Turn(Rotation r) { m_rotationTarget += r; } public void TurnTowards(float yaw, float pitch, float factor = 1f) { TurnTowards(new Rotation(pitch, yaw)); } public void TurnTowards(Vector3 targetPosition, float factor = 1f) { TurnTowards(new Rotation(targetPosition - transform.position)); } public void TurnTowards(Rotation target, float factor = 1f) { m_rotationTarget = Rotation.Rotate(m_rotationTarget, target, m_rotationSpeed * factor * Time.deltaTime); } public void Accelerate(Vector3 direction) { Accelerate(direction, m_accelerationRange.Lerp(pitchValue)); } public void Accelerate(Vector3 direction, float factor) { Vector3 delta = m_rotation * direction.normalized * (m_acceleration * factor); m_velocity += delta * Time.deltaTime; } public void SetBehaviour(int index) { if (index == -1) index = m_behaviourIndex + 1; index = index % m_behaviourList.Length; if (index != m_behaviourIndex) { currentBehaviour.EndBehaviour(); m_behaviourIndex = index % m_behaviourList.Length; currentBehaviour.BeginBehaviour(); } } public void SetBehaviour(Type t) { WispBehaviour behaviour = (WispBehaviour)GetComponent(t); int index = Array.FindIndex(m_behaviourList, (x) => x == behaviour); if (index != -1) SetBehaviour(index); } void Update() { if (currentBehaviour) currentBehaviour.DoBehaviour(); DoTurning(); DoFriction(); DoMovement(); DoIdleVariations(); } void DoGroundDetection() { if (m_velocity.y > 4f) return; RaycastHit hit; if (Physics.Raycast(transform.position, Vector3.down, out hit, 6f)) { if (hit.collider.isTrigger) return; float distanceFactor = 1f - hit.distance / 6f; m_rotationTarget.pitch = -40f * distanceFactor; if (m_velocity.y < 0f) m_velocity.y = Mathf.Lerp(m_velocity.y, 0f, distanceFactor * 8f * Time.deltaTime); } } void DoTurning() { DoGroundDetection(); float oldYaw = m_rotation.yaw; m_rotation = Rotation.Lerp(m_rotation, m_rotationTarget + drift * driftAmount, m_rotationLerp * Time.deltaTime); m_rollTarget = -(m_rotation.yaw - oldYaw) * m_rollFactor / Time.deltaTime; m_rollTarget = PolarUtils.MinMagnitude(m_rollTarget, Mathf.Sign(m_rollTarget) * m_rollMax); m_roll = Mathf.Lerp(m_roll, m_rollTarget, 0.7f * Time.deltaTime); transform.rotation = Quaternion.Euler(m_rotation.pitch, m_rotation.yaw, m_roll); } public void SetBehaviourOverride(WispBehaviour behaviour) { if (currentBehaviour == behaviour) return; currentBehaviour.EndBehaviour(); m_behaviourOverride = behaviour.CopyTo(this); currentBehaviour.BeginBehaviour(); } public void ClearBehaviourOverride() { if (!m_behaviourOverride) return; currentBehaviour.EndBehaviour(); Destroy(m_behaviourOverride, 10f); m_behaviourOverride = null; currentBehaviour.BeginBehaviour(); } void DoFriction() { m_velocity -= m_velocity * m_friction * Time.deltaTime; } void DoMovement() { transform.Translate(m_velocity * Time.deltaTime, Space.World); } void DoIdleVariations() { m_idleTimer.Update(); if (m_idleTimer.Done) { Animator animator = GetComponentInChildren<Animator>(); if (animator) { animator.SetInteger("VariationNmbr", UnityEngine.Random.Range(1, 5)); animator.SetTrigger("Variation"); } m_idleTimer.Reset(); } } void OnTriggerEnter(Collider other) { if (!other.isTrigger && m_collisionParticle) Instantiate(m_collisionParticle, transform.position, Quaternion.identity); } void OnTriggerExit(Collider other) { if (!other.isTrigger && m_collisionParticle) Instantiate(m_collisionParticle, transform.position, Quaternion.identity); } }
using UnityEngine; using System.Collections; using System.Reflection; [System.Serializable] public abstract class WispBehaviour : MonoBehaviour { protected Wisp m_wisp; public virtual float driftAmount { get { return 1f; } } protected virtual void Start() { m_wisp = GetComponent<Wisp>(); } public abstract void DoBehaviour(); public virtual void BeginBehaviour() { } public virtual void EndBehaviour() { } public WispBehaviour CopyTo(Wisp target) { System.Type type = this.GetType(); WispBehaviour behaviour = (WispBehaviour)target.gameObject.AddComponent(type); FieldInfo[] fields = type.GetFields(BindingFlags.Instance | BindingFlags.NonPublic); foreach (FieldInfo field in fields) field.SetValue(behaviour, field.GetValue(this)); return behaviour; } }
The snow deforms when the player walks over it, creating persistent trails.
This is achieved by manipulating and deforming the Unity terrain.
using UnityEngine; using System; using System.Collections; public class SnowTerrain : MonoBehaviour { Terrain m_terrain; TerrainData m_beginData; TerrainData m_tempData; float m_snowDepth = 0.0004f; public int resolution { get { return m_tempData.heightmapResolution; } } public delegate float DigFunction(float x, float y, float current, float begin); // Use this for initialization void Start() { m_terrain = GetComponent<Terrain>(); m_beginData = m_terrain.terrainData; m_tempData = Instantiate(m_beginData); m_terrain.terrainData = m_tempData; InvokeRepeating("Apply", 1f, 20f); } void Apply() { m_terrain.ApplyDelayedHeightmapModification(); } public bool InsideBounds(int coord) { return coord >= 0 && coord < m_tempData.heightmapResolution; } //Clamp 1Dimensional coordinate and size to fit on the heightmap public void ClampToBounds(ref int coord, ref int size, bool resize = true) { int oldCoord = coord; coord = Mathf.Clamp(coord, 0, resolution); if (resize) size -= (coord - oldCoord); size = Mathf.Min(size, resolution - coord); } //Clamp 2Dimensional position and size to fit on the heightmap public void ClampToBounds(ref int x, ref int y, ref int width, ref int height, bool resize = true) { ClampToBounds(ref x, ref width, resize); ClampToBounds(ref y, ref height, resize); } public void Dig(Vector3 world, Vector2 size) { int x, y; int width = (int)((size.x / m_beginData.size.x) * resolution); int height = (int)((size.y / m_beginData.size.z) * resolution); if (Project(world, out x, out y)) { // Function is a just a simple paraboloid SetHeightFunc(x - width/2 - 1, y - height/2 - 1, width+1, height+1, (fx, fy, c, b) => Mathf.Min(c, b - Mathf.Max(m_snowDepth * (1f - (fx * fx + fy * fy)), 0f))); } } //Project a world point onto the terrain, and get the height-map pixel corresponding to that point //Returns if the projection lands on the plane public bool Project(Vector3 world, out int x, out int y) { Vector3 offset = world - transform.position; offset.x /= m_tempData.size.x; offset.z /= m_tempData.size.z; offset *= resolution; x = Mathf.RoundToInt(offset.x); y = Mathf.RoundToInt(offset.z); return (x >= 0 && x <= resolution && y >= 0 && y <= resolution); } public void SetHeightFunc(int x, int y, int width, int height, DigFunction function) { ClampToBounds(ref x, ref y, ref width, ref height, true); //The 2-dimensional array is arranged [y, x], remember that float[,] beginHeightList = m_beginData.GetHeights(x, y, width, height); float[,] currentHeightList = m_tempData.GetHeights(x, y, width, height); float[,] newHeightList = new float[height, width]; for (int i = 0; i < width; i++) for (int j = 0; j < height; j++) { float xFunc = ((float)i / width - 0.5f) * 2f; float yFunc = ((float)j / height - 0.5f) * 2f; float h = function(xFunc, yFunc, currentHeightList[j, i], beginHeightList[j, i]); //That's why I flip the values here newHeightList[j, i] = h; } m_tempData.SetHeightsDelayLOD(x, y, newHeightList); } }