Import 2D Level
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Platformer.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Fuzzy provides methods for using values +- an amount of random deviation, or fuzz.
|
||||
/// </summary>
|
||||
static class Fuzzy
|
||||
{
|
||||
public static bool ValueLessThan(float value, float test, float fuzz = 0.1f)
|
||||
{
|
||||
var delta = value - test;
|
||||
return delta < 0 ? true : Random.value > delta / (fuzz * test);
|
||||
}
|
||||
|
||||
public static bool ValueGreaterThan(float value, float test, float fuzz = 0.1f)
|
||||
{
|
||||
var delta = value - test;
|
||||
return delta < 0 ? Random.value > -1 * delta / (fuzz * test) : true;
|
||||
}
|
||||
|
||||
public static bool ValueNear(float value, float test, float fuzz = 0.1f)
|
||||
{
|
||||
return Mathf.Abs(1f - (value / test)) < fuzz;
|
||||
}
|
||||
|
||||
public static float Value(float value, float fuzz = 0.1f)
|
||||
{
|
||||
return value + value * Random.Range(-fuzz, +fuzz);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Platformer.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// HeapQueue provides a queue collection that is always ordered.
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
public class HeapQueue<T> where T : IComparable<T>
|
||||
{
|
||||
List<T> items;
|
||||
|
||||
public int Count { get { return items.Count; } }
|
||||
|
||||
public bool IsEmpty { get { return items.Count == 0; } }
|
||||
|
||||
public T First { get { return items[0]; } }
|
||||
|
||||
public void Clear() => items.Clear();
|
||||
|
||||
public bool Contains(T item) => items.Contains(item);
|
||||
|
||||
public void Remove(T item) => items.Remove(item);
|
||||
|
||||
public T Peek() => items[0];
|
||||
|
||||
public HeapQueue()
|
||||
{
|
||||
items = new List<T>();
|
||||
}
|
||||
|
||||
public void Push(T item)
|
||||
{
|
||||
//add item to end of tree to extend the list
|
||||
items.Add(item);
|
||||
//find correct position for new item.
|
||||
SiftDown(0, items.Count - 1);
|
||||
}
|
||||
|
||||
public T Pop()
|
||||
{
|
||||
|
||||
//if there are more than 1 items, returned item will be first in tree.
|
||||
//then, add last item to front of tree, shrink the list
|
||||
//and find correct index in tree for first item.
|
||||
T item;
|
||||
var last = items[items.Count - 1];
|
||||
items.RemoveAt(items.Count - 1);
|
||||
if (items.Count > 0)
|
||||
{
|
||||
item = items[0];
|
||||
items[0] = last;
|
||||
SiftUp();
|
||||
}
|
||||
else
|
||||
{
|
||||
item = last;
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
|
||||
int Compare(T A, T B) => A.CompareTo(B);
|
||||
|
||||
void SiftDown(int startpos, int pos)
|
||||
{
|
||||
//preserve the newly added item.
|
||||
var newitem = items[pos];
|
||||
while (pos > startpos)
|
||||
{
|
||||
//find parent index in binary tree
|
||||
var parentpos = (pos - 1) >> 1;
|
||||
var parent = items[parentpos];
|
||||
//if new item precedes or equal to parent, pos is new item position.
|
||||
if (Compare(parent, newitem) <= 0)
|
||||
break;
|
||||
//else move parent into pos, then repeat for grand parent.
|
||||
items[pos] = parent;
|
||||
pos = parentpos;
|
||||
}
|
||||
items[pos] = newitem;
|
||||
}
|
||||
|
||||
void SiftUp()
|
||||
{
|
||||
var endpos = items.Count;
|
||||
var startpos = 0;
|
||||
//preserve the inserted item
|
||||
var newitem = items[0];
|
||||
var childpos = 1;
|
||||
var pos = 0;
|
||||
//find child position to insert into binary tree
|
||||
while (childpos < endpos)
|
||||
{
|
||||
//get right branch
|
||||
var rightpos = childpos + 1;
|
||||
//if right branch should precede left branch, move right branch up the tree
|
||||
if (rightpos < endpos && Compare(items[rightpos], items[childpos]) <= 0)
|
||||
childpos = rightpos;
|
||||
//move child up the tree
|
||||
items[pos] = items[childpos];
|
||||
pos = childpos;
|
||||
//move down the tree and repeat.
|
||||
childpos = 2 * pos + 1;
|
||||
}
|
||||
//the child position for the new item.
|
||||
items[pos] = newitem;
|
||||
SiftDown(startpos, pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Platformer.Core
|
||||
{
|
||||
public static partial class Simulation
|
||||
{
|
||||
/// <summary>
|
||||
/// An event is something that happens at a point in time in a simulation.
|
||||
/// The Precondition method is used to check if the event should be executed,
|
||||
/// as conditions may have changed in the simulation since the event was
|
||||
/// originally scheduled.
|
||||
/// </summary>
|
||||
/// <typeparam name="Event"></typeparam>
|
||||
public abstract class Event : System.IComparable<Event>
|
||||
{
|
||||
internal float tick;
|
||||
|
||||
public int CompareTo(Event other)
|
||||
{
|
||||
return tick.CompareTo(other.tick);
|
||||
}
|
||||
|
||||
public abstract void Execute();
|
||||
|
||||
public virtual bool Precondition() => true;
|
||||
|
||||
internal virtual void ExecuteEvent()
|
||||
{
|
||||
if (Precondition())
|
||||
Execute();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This method is generally used to set references to null when required.
|
||||
/// It is automatically called by the Simulation when an event has completed.
|
||||
/// </summary>
|
||||
internal virtual void Cleanup()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event<T> adds the ability to hook into the OnExecute callback
|
||||
/// whenever the event is executed. Use this class to allow functionality
|
||||
/// to be plugged into your application with minimal or zero configuration.
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
public abstract class Event<T> : Event where T : Event<T>
|
||||
{
|
||||
public static System.Action<T> OnExecute;
|
||||
|
||||
internal override void ExecuteEvent()
|
||||
{
|
||||
if (Precondition())
|
||||
{
|
||||
Execute();
|
||||
OnExecute?.Invoke((T)this);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace Platformer.Core
|
||||
{
|
||||
public static partial class Simulation
|
||||
{
|
||||
/// <summary>
|
||||
/// This class provides a container for creating singletons for any other class,
|
||||
/// within the scope of the Simulation. It is typically used to hold the simulation
|
||||
/// models and configuration classes.
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
static class InstanceRegister<T> where T : class, new()
|
||||
{
|
||||
public static T instance = new T();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
|
||||
namespace Platformer.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// The Simulation class implements the discrete event simulator pattern.
|
||||
/// Events are pooled, with a default capacity of 4 instances.
|
||||
/// </summary>
|
||||
public static partial class Simulation
|
||||
{
|
||||
|
||||
static HeapQueue<Event> eventQueue = new HeapQueue<Event>();
|
||||
static Dictionary<System.Type, Stack<Event>> eventPools = new Dictionary<System.Type, Stack<Event>>();
|
||||
|
||||
/// <summary>
|
||||
/// Create a new event of type T and return it, but do not schedule it.
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
static public T New<T>() where T : Event, new()
|
||||
{
|
||||
Stack<Event> pool;
|
||||
if (!eventPools.TryGetValue(typeof(T), out pool))
|
||||
{
|
||||
pool = new Stack<Event>(4);
|
||||
pool.Push(new T());
|
||||
eventPools[typeof(T)] = pool;
|
||||
}
|
||||
if (pool.Count > 0)
|
||||
return (T)pool.Pop();
|
||||
else
|
||||
return new T();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear all pending events and reset the tick to 0.
|
||||
/// </summary>
|
||||
public static void Clear()
|
||||
{
|
||||
eventQueue.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Schedule an event for a future tick, and return it.
|
||||
/// </summary>
|
||||
/// <returns>The event.</returns>
|
||||
/// <param name="tick">Tick.</param>
|
||||
/// <typeparam name="T">The event type parameter.</typeparam>
|
||||
static public T Schedule<T>(float tick = 0) where T : Event, new()
|
||||
{
|
||||
var ev = New<T>();
|
||||
ev.tick = Time.time + tick;
|
||||
eventQueue.Push(ev);
|
||||
return ev;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reschedule an existing event for a future tick, and return it.
|
||||
/// </summary>
|
||||
/// <returns>The event.</returns>
|
||||
/// <param name="tick">Tick.</param>
|
||||
/// <typeparam name="T">The event type parameter.</typeparam>
|
||||
static public T Reschedule<T>(T ev, float tick) where T : Event, new()
|
||||
{
|
||||
ev.tick = Time.time + tick;
|
||||
eventQueue.Push(ev);
|
||||
return ev;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return the simulation model instance for a class.
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
static public T GetModel<T>() where T : class, new()
|
||||
{
|
||||
return InstanceRegister<T>.instance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set a simulation model instance for a class.
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
static public void SetModel<T>(T instance) where T : class, new()
|
||||
{
|
||||
InstanceRegister<T>.instance = instance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Destroy the simulation model instance for a class.
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
static public void DestroyModel<T>() where T : class, new()
|
||||
{
|
||||
InstanceRegister<T>.instance = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tick the simulation. Returns the count of remaining events.
|
||||
/// If remaining events is zero, the simulation is finished unless events are
|
||||
/// injected from an external system via a Schedule() call.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
static public int Tick()
|
||||
{
|
||||
var time = Time.time;
|
||||
var executedEventCount = 0;
|
||||
while (eventQueue.Count > 0 && eventQueue.Peek().tick <= time)
|
||||
{
|
||||
var ev = eventQueue.Pop();
|
||||
var tick = ev.tick;
|
||||
ev.ExecuteEvent();
|
||||
if (ev.tick > tick)
|
||||
{
|
||||
//event was rescheduled, so do not return it to the pool.
|
||||
}
|
||||
else
|
||||
{
|
||||
// Debug.Log($"<color=green>{ev.tick} {ev.GetType().Name}</color>");
|
||||
ev.Cleanup();
|
||||
try
|
||||
{
|
||||
eventPools[ev.GetType()].Push(ev);
|
||||
}
|
||||
catch (KeyNotFoundException)
|
||||
{
|
||||
//This really should never happen inside a production build.
|
||||
Debug.LogError($"No Pool for: {ev.GetType()}");
|
||||
}
|
||||
}
|
||||
executedEventCount++;
|
||||
}
|
||||
return eventQueue.Count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
using Platformer.Core;
|
||||
using Platformer.Model;
|
||||
|
||||
namespace Platformer.Gameplay
|
||||
{
|
||||
/// <summary>
|
||||
/// This event is fired when user input should be enabled.
|
||||
/// </summary>
|
||||
public class EnablePlayerInput : Simulation.Event<EnablePlayerInput>
|
||||
{
|
||||
PlatformerModel model = Simulation.GetModel<PlatformerModel>();
|
||||
|
||||
public override void Execute()
|
||||
{
|
||||
var player = model.player;
|
||||
player.controlEnabled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using Platformer.Core;
|
||||
using Platformer.Mechanics;
|
||||
|
||||
namespace Platformer.Gameplay
|
||||
{
|
||||
/// <summary>
|
||||
/// Fired when the health component on an enemy has a hitpoint value of 0.
|
||||
/// </summary>
|
||||
/// <typeparam name="EnemyDeath"></typeparam>
|
||||
public class EnemyDeath : Simulation.Event<EnemyDeath>
|
||||
{
|
||||
public EnemyController enemy;
|
||||
|
||||
public override void Execute()
|
||||
{
|
||||
enemy._collider.enabled = false;
|
||||
enemy.control.enabled = false;
|
||||
if (enemy._audio && enemy.ouch)
|
||||
enemy._audio.PlayOneShot(enemy.ouch);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using Platformer.Core;
|
||||
using Platformer.Mechanics;
|
||||
using static Platformer.Core.Simulation;
|
||||
|
||||
namespace Platformer.Gameplay
|
||||
{
|
||||
/// <summary>
|
||||
/// Fired when the player health reaches 0. This usually would result in a
|
||||
/// PlayerDeath event.
|
||||
/// </summary>
|
||||
/// <typeparam name="HealthIsZero"></typeparam>
|
||||
public class HealthIsZero : Simulation.Event<HealthIsZero>
|
||||
{
|
||||
public Health health;
|
||||
|
||||
public override void Execute()
|
||||
{
|
||||
Schedule<PlayerDeath>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using Platformer.Core;
|
||||
using Platformer.Model;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Platformer.Gameplay
|
||||
{
|
||||
/// <summary>
|
||||
/// Fired when the player has died.
|
||||
/// </summary>
|
||||
/// <typeparam name="PlayerDeath"></typeparam>
|
||||
public class PlayerDeath : Simulation.Event<PlayerDeath>
|
||||
{
|
||||
PlatformerModel model = Simulation.GetModel<PlatformerModel>();
|
||||
|
||||
public override void Execute()
|
||||
{
|
||||
var player = model.player;
|
||||
if (player.health.IsAlive)
|
||||
{
|
||||
player.health.Die();
|
||||
model.virtualCamera.Follow = null;
|
||||
model.virtualCamera.LookAt = null;
|
||||
// player.collider.enabled = false;
|
||||
player.controlEnabled = false;
|
||||
|
||||
if (player.audioSource && player.ouchAudio)
|
||||
player.audioSource.PlayOneShot(player.ouchAudio);
|
||||
player.animator.SetTrigger("hurt");
|
||||
player.animator.SetBool("dead", true);
|
||||
Simulation.Schedule<PlayerSpawn>(2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using Platformer.Core;
|
||||
using Platformer.Mechanics;
|
||||
using Platformer.Model;
|
||||
using UnityEngine;
|
||||
using static Platformer.Core.Simulation;
|
||||
|
||||
namespace Platformer.Gameplay
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Fired when a Player collides with an Enemy.
|
||||
/// </summary>
|
||||
/// <typeparam name="EnemyCollision"></typeparam>
|
||||
public class PlayerEnemyCollision : Simulation.Event<PlayerEnemyCollision>
|
||||
{
|
||||
public EnemyController enemy;
|
||||
public PlayerController player;
|
||||
|
||||
PlatformerModel model = Simulation.GetModel<PlatformerModel>();
|
||||
|
||||
public override void Execute()
|
||||
{
|
||||
var willHurtEnemy = player.Bounds.center.y >= enemy.Bounds.max.y;
|
||||
|
||||
if (willHurtEnemy)
|
||||
{
|
||||
var enemyHealth = enemy.GetComponent<Health>();
|
||||
if (enemyHealth != null)
|
||||
{
|
||||
enemyHealth.Decrement();
|
||||
if (!enemyHealth.IsAlive)
|
||||
{
|
||||
Schedule<EnemyDeath>().enemy = enemy;
|
||||
player.Bounce(2);
|
||||
}
|
||||
else
|
||||
{
|
||||
player.Bounce(7);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Schedule<EnemyDeath>().enemy = enemy;
|
||||
player.Bounce(2);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Schedule<PlayerDeath>();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using Platformer.Core;
|
||||
using Platformer.Mechanics;
|
||||
using Platformer.Model;
|
||||
|
||||
namespace Platformer.Gameplay
|
||||
{
|
||||
/// <summary>
|
||||
/// Fired when a player enters a trigger with a DeathZone component.
|
||||
/// </summary>
|
||||
/// <typeparam name="PlayerEnteredDeathZone"></typeparam>
|
||||
public class PlayerEnteredDeathZone : Simulation.Event<PlayerEnteredDeathZone>
|
||||
{
|
||||
public DeathZone deathzone;
|
||||
|
||||
PlatformerModel model = Simulation.GetModel<PlatformerModel>();
|
||||
|
||||
public override void Execute()
|
||||
{
|
||||
Simulation.Schedule<PlayerDeath>(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using Platformer.Core;
|
||||
using Platformer.Mechanics;
|
||||
using Platformer.Model;
|
||||
|
||||
namespace Platformer.Gameplay
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// This event is triggered when the player character enters a trigger with a VictoryZone component.
|
||||
/// </summary>
|
||||
/// <typeparam name="PlayerEnteredVictoryZone"></typeparam>
|
||||
public class PlayerEnteredVictoryZone : Simulation.Event<PlayerEnteredVictoryZone>
|
||||
{
|
||||
public VictoryZone victoryZone;
|
||||
|
||||
PlatformerModel model = Simulation.GetModel<PlatformerModel>();
|
||||
|
||||
public override void Execute()
|
||||
{
|
||||
model.player.animator.SetTrigger("victory");
|
||||
model.player.controlEnabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using Platformer.Core;
|
||||
using Platformer.Mechanics;
|
||||
|
||||
namespace Platformer.Gameplay
|
||||
{
|
||||
/// <summary>
|
||||
/// Fired when the player performs a Jump.
|
||||
/// </summary>
|
||||
/// <typeparam name="PlayerJumped"></typeparam>
|
||||
public class PlayerJumped : Simulation.Event<PlayerJumped>
|
||||
{
|
||||
public PlayerController player;
|
||||
|
||||
public override void Execute()
|
||||
{
|
||||
if (player.audioSource && player.jumpAudio)
|
||||
player.audioSource.PlayOneShot(player.jumpAudio);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using Platformer.Core;
|
||||
using Platformer.Mechanics;
|
||||
|
||||
namespace Platformer.Gameplay
|
||||
{
|
||||
/// <summary>
|
||||
/// Fired when the player character lands after being airborne.
|
||||
/// </summary>
|
||||
/// <typeparam name="PlayerLanded"></typeparam>
|
||||
public class PlayerLanded : Simulation.Event<PlayerLanded>
|
||||
{
|
||||
public PlayerController player;
|
||||
|
||||
public override void Execute()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using Platformer.Core;
|
||||
using Platformer.Mechanics;
|
||||
using Platformer.Model;
|
||||
|
||||
namespace Platformer.Gameplay
|
||||
{
|
||||
/// <summary>
|
||||
/// Fired when the player is spawned after dying.
|
||||
/// </summary>
|
||||
public class PlayerSpawn : Simulation.Event<PlayerSpawn>
|
||||
{
|
||||
PlatformerModel model = Simulation.GetModel<PlatformerModel>();
|
||||
|
||||
public override void Execute()
|
||||
{
|
||||
var player = model.player;
|
||||
player.collider2d.enabled = true;
|
||||
player.controlEnabled = false;
|
||||
if (player.audioSource && player.respawnAudio)
|
||||
player.audioSource.PlayOneShot(player.respawnAudio);
|
||||
player.health.Increment();
|
||||
player.Teleport(model.spawnPoint.transform.position);
|
||||
player.jumpState = PlayerController.JumpState.Grounded;
|
||||
player.animator.SetBool("dead", false);
|
||||
model.virtualCamera.Follow = player.transform;
|
||||
model.virtualCamera.LookAt = player.transform;
|
||||
Simulation.Schedule<EnablePlayerInput>(2f);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using Platformer.Core;
|
||||
using Platformer.Mechanics;
|
||||
|
||||
namespace Platformer.Gameplay
|
||||
{
|
||||
/// <summary>
|
||||
/// Fired when the Jump Input is deactivated by the user, cancelling the upward velocity of the jump.
|
||||
/// </summary>
|
||||
/// <typeparam name="PlayerStopJump"></typeparam>
|
||||
public class PlayerStopJump : Simulation.Event<PlayerStopJump>
|
||||
{
|
||||
public PlayerController player;
|
||||
|
||||
public override void Execute()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using Platformer.Core;
|
||||
using Platformer.Mechanics;
|
||||
using Platformer.Model;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Platformer.Gameplay
|
||||
{
|
||||
/// <summary>
|
||||
/// Fired when a player collides with a token.
|
||||
/// </summary>
|
||||
/// <typeparam name="PlayerCollision"></typeparam>
|
||||
public class PlayerTokenCollision : Simulation.Event<PlayerTokenCollision>
|
||||
{
|
||||
public PlayerController player;
|
||||
public TokenInstance token;
|
||||
|
||||
PlatformerModel model = Simulation.GetModel<PlatformerModel>();
|
||||
|
||||
public override void Execute()
|
||||
{
|
||||
AudioSource.PlayClipAtPoint(token.tokenCollectAudio, token.transform.position);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using Platformer.Core;
|
||||
using Platformer.Model;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Platformer.Mechanics
|
||||
{
|
||||
/// <summary>
|
||||
/// AnimationController integrates physics and animation. It is generally used for simple enemy animation.
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(SpriteRenderer), typeof(Animator))]
|
||||
public class AnimationController : KinematicObject
|
||||
{
|
||||
/// <summary>
|
||||
/// Max horizontal speed.
|
||||
/// </summary>
|
||||
public float maxSpeed = 7;
|
||||
/// <summary>
|
||||
/// Max jump velocity
|
||||
/// </summary>
|
||||
public float jumpTakeOffSpeed = 7;
|
||||
|
||||
/// <summary>
|
||||
/// Used to indicated desired direction of travel.
|
||||
/// </summary>
|
||||
public Vector2 move;
|
||||
|
||||
/// <summary>
|
||||
/// Set to true to initiate a jump.
|
||||
/// </summary>
|
||||
public bool jump;
|
||||
|
||||
/// <summary>
|
||||
/// Set to true to set the current jump velocity to zero.
|
||||
/// </summary>
|
||||
public bool stopJump;
|
||||
|
||||
SpriteRenderer spriteRenderer;
|
||||
Animator animator;
|
||||
PlatformerModel model = Simulation.GetModel<PlatformerModel>();
|
||||
|
||||
protected virtual void Awake()
|
||||
{
|
||||
spriteRenderer = GetComponent<SpriteRenderer>();
|
||||
animator = GetComponent<Animator>();
|
||||
}
|
||||
|
||||
protected override void ComputeVelocity()
|
||||
{
|
||||
if (jump && IsGrounded)
|
||||
{
|
||||
velocity.y = jumpTakeOffSpeed * model.jumpModifier;
|
||||
jump = false;
|
||||
}
|
||||
else if (stopJump)
|
||||
{
|
||||
stopJump = false;
|
||||
if (velocity.y > 0)
|
||||
{
|
||||
velocity.y = velocity.y * model.jumpDeceleration;
|
||||
}
|
||||
}
|
||||
|
||||
if (move.x > 0.01f)
|
||||
spriteRenderer.flipX = false;
|
||||
else if (move.x < -0.01f)
|
||||
spriteRenderer.flipX = true;
|
||||
|
||||
animator.SetBool("grounded", IsGrounded);
|
||||
animator.SetFloat("velocityX", Mathf.Abs(velocity.x) / maxSpeed);
|
||||
|
||||
targetVelocity = move * maxSpeed;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using Platformer.Gameplay;
|
||||
using UnityEngine;
|
||||
using static Platformer.Core.Simulation;
|
||||
|
||||
namespace Platformer.Mechanics
|
||||
{
|
||||
/// <summary>
|
||||
/// DeathZone components mark a collider which will schedule a
|
||||
/// PlayerEnteredDeathZone event when the player enters the trigger.
|
||||
/// </summary>
|
||||
public class DeathZone : MonoBehaviour
|
||||
{
|
||||
void OnTriggerEnter2D(Collider2D collider)
|
||||
{
|
||||
var p = collider.gameObject.GetComponent<PlayerController>();
|
||||
if (p != null)
|
||||
{
|
||||
var ev = Schedule<PlayerEnteredDeathZone>();
|
||||
ev.deathzone = this;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using Platformer.Gameplay;
|
||||
using UnityEngine;
|
||||
using static Platformer.Core.Simulation;
|
||||
|
||||
namespace Platformer.Mechanics
|
||||
{
|
||||
/// <summary>
|
||||
/// A simple controller for enemies. Provides movement control over a patrol path.
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(AnimationController), typeof(Collider2D))]
|
||||
public class EnemyController : MonoBehaviour
|
||||
{
|
||||
public PatrolPath path;
|
||||
public AudioClip ouch;
|
||||
|
||||
internal PatrolPath.Mover mover;
|
||||
internal AnimationController control;
|
||||
internal Collider2D _collider;
|
||||
internal AudioSource _audio;
|
||||
SpriteRenderer spriteRenderer;
|
||||
|
||||
public Bounds Bounds => _collider.bounds;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
control = GetComponent<AnimationController>();
|
||||
_collider = GetComponent<Collider2D>();
|
||||
_audio = GetComponent<AudioSource>();
|
||||
spriteRenderer = GetComponent<SpriteRenderer>();
|
||||
}
|
||||
|
||||
void OnCollisionEnter2D(Collision2D collision)
|
||||
{
|
||||
var player = collision.gameObject.GetComponent<PlayerController>();
|
||||
if (player != null)
|
||||
{
|
||||
var ev = Schedule<PlayerEnemyCollision>();
|
||||
ev.player = player;
|
||||
ev.enemy = this;
|
||||
}
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (path != null)
|
||||
{
|
||||
if (mover == null) mover = path.CreateMover(control.maxSpeed * 0.5f);
|
||||
control.move.x = Mathf.Clamp(mover.Position.x - transform.position.x, -1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using Platformer.Core;
|
||||
using Platformer.Model;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Platformer.Mechanics
|
||||
{
|
||||
/// <summary>
|
||||
/// This class exposes the the game model in the inspector, and ticks the
|
||||
/// simulation.
|
||||
/// </summary>
|
||||
public class GameController : MonoBehaviour
|
||||
{
|
||||
public static GameController Instance { get; private set; }
|
||||
|
||||
//This model field is public and can be therefore be modified in the
|
||||
//inspector.
|
||||
//The reference actually comes from the InstanceRegister, and is shared
|
||||
//through the simulation and events. Unity will deserialize over this
|
||||
//shared reference when the scene loads, allowing the model to be
|
||||
//conveniently configured inside the inspector.
|
||||
public PlatformerModel model = Simulation.GetModel<PlatformerModel>();
|
||||
|
||||
void OnEnable()
|
||||
{
|
||||
Instance = this;
|
||||
}
|
||||
|
||||
void OnDisable()
|
||||
{
|
||||
if (Instance == this) Instance = null;
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (Instance == this) Simulation.Tick();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using System;
|
||||
using Platformer.Gameplay;
|
||||
using UnityEngine;
|
||||
using static Platformer.Core.Simulation;
|
||||
|
||||
namespace Platformer.Mechanics
|
||||
{
|
||||
/// <summary>
|
||||
/// Represebts the current vital statistics of some game entity.
|
||||
/// </summary>
|
||||
public class Health : MonoBehaviour
|
||||
{
|
||||
/// <summary>
|
||||
/// The maximum hit points for the entity.
|
||||
/// </summary>
|
||||
public int maxHP = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Indicates if the entity should be considered 'alive'.
|
||||
/// </summary>
|
||||
public bool IsAlive => currentHP > 0;
|
||||
|
||||
int currentHP;
|
||||
|
||||
/// <summary>
|
||||
/// Increment the HP of the entity.
|
||||
/// </summary>
|
||||
public void Increment()
|
||||
{
|
||||
currentHP = Mathf.Clamp(currentHP + 1, 0, maxHP);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decrement the HP of the entity. Will trigger a HealthIsZero event when
|
||||
/// current HP reaches 0.
|
||||
/// </summary>
|
||||
public void Decrement()
|
||||
{
|
||||
currentHP = Mathf.Clamp(currentHP - 1, 0, maxHP);
|
||||
if (currentHP == 0)
|
||||
{
|
||||
var ev = Schedule<HealthIsZero>();
|
||||
ev.health = this;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decrement the HP of the entitiy until HP reaches 0.
|
||||
/// </summary>
|
||||
public void Die()
|
||||
{
|
||||
while (currentHP > 0) Decrement();
|
||||
}
|
||||
|
||||
void Awake()
|
||||
{
|
||||
currentHP = maxHP;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Platformer.Mechanics
|
||||
{
|
||||
/// <summary>
|
||||
/// Implements game physics for some in game entity.
|
||||
/// </summary>
|
||||
public class KinematicObject : MonoBehaviour
|
||||
{
|
||||
/// <summary>
|
||||
/// The minimum normal (dot product) considered suitable for the entity sit on.
|
||||
/// </summary>
|
||||
public float minGroundNormalY = .65f;
|
||||
|
||||
/// <summary>
|
||||
/// A custom gravity coefficient applied to this entity.
|
||||
/// </summary>
|
||||
public float gravityModifier = 1f;
|
||||
|
||||
/// <summary>
|
||||
/// The current velocity of the entity.
|
||||
/// </summary>
|
||||
public Vector2 velocity;
|
||||
|
||||
/// <summary>
|
||||
/// Is the entity currently sitting on a surface?
|
||||
/// </summary>
|
||||
/// <value></value>
|
||||
public bool IsGrounded { get; private set; }
|
||||
|
||||
protected Vector2 targetVelocity;
|
||||
protected Vector2 groundNormal;
|
||||
protected Rigidbody2D body;
|
||||
protected ContactFilter2D contactFilter;
|
||||
protected RaycastHit2D[] hitBuffer = new RaycastHit2D[16];
|
||||
|
||||
protected const float minMoveDistance = 0.001f;
|
||||
protected const float shellRadius = 0.01f;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Bounce the object's vertical velocity.
|
||||
/// </summary>
|
||||
/// <param name="value"></param>
|
||||
public void Bounce(float value)
|
||||
{
|
||||
velocity.y = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bounce the objects velocity in a direction.
|
||||
/// </summary>
|
||||
/// <param name="dir"></param>
|
||||
public void Bounce(Vector2 dir)
|
||||
{
|
||||
velocity.y = dir.y;
|
||||
velocity.x = dir.x;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Teleport to some position.
|
||||
/// </summary>
|
||||
/// <param name="position"></param>
|
||||
public void Teleport(Vector3 position)
|
||||
{
|
||||
body.position = position;
|
||||
velocity *= 0;
|
||||
body.linearVelocity *= 0;
|
||||
}
|
||||
|
||||
protected virtual void OnEnable()
|
||||
{
|
||||
body = GetComponent<Rigidbody2D>();
|
||||
body.bodyType = RigidbodyType2D.Kinematic;
|
||||
}
|
||||
|
||||
protected virtual void OnDisable()
|
||||
{
|
||||
body.bodyType = RigidbodyType2D.Dynamic;
|
||||
}
|
||||
|
||||
protected virtual void Start()
|
||||
{
|
||||
contactFilter.useTriggers = false;
|
||||
contactFilter.SetLayerMask(Physics2D.GetLayerCollisionMask(gameObject.layer));
|
||||
contactFilter.useLayerMask = true;
|
||||
}
|
||||
|
||||
protected virtual void Update()
|
||||
{
|
||||
targetVelocity = Vector2.zero;
|
||||
ComputeVelocity();
|
||||
}
|
||||
|
||||
protected virtual void ComputeVelocity()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
protected virtual void FixedUpdate()
|
||||
{
|
||||
//if already falling, fall faster than the jump speed, otherwise use normal gravity.
|
||||
if (velocity.y < 0)
|
||||
velocity += gravityModifier * Physics2D.gravity * Time.deltaTime;
|
||||
else
|
||||
velocity += Physics2D.gravity * Time.deltaTime;
|
||||
|
||||
velocity.x = targetVelocity.x;
|
||||
|
||||
IsGrounded = false;
|
||||
|
||||
var deltaPosition = velocity * Time.deltaTime;
|
||||
|
||||
var moveAlongGround = new Vector2(groundNormal.y, -groundNormal.x);
|
||||
|
||||
var move = moveAlongGround * deltaPosition.x;
|
||||
|
||||
PerformMovement(move, false);
|
||||
|
||||
move = Vector2.up * deltaPosition.y;
|
||||
|
||||
PerformMovement(move, true);
|
||||
|
||||
}
|
||||
|
||||
void PerformMovement(Vector2 move, bool yMovement)
|
||||
{
|
||||
var distance = move.magnitude;
|
||||
|
||||
if (distance > minMoveDistance)
|
||||
{
|
||||
//check if we hit anything in current direction of travel
|
||||
var count = body.Cast(move, contactFilter, hitBuffer, distance + shellRadius);
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var currentNormal = hitBuffer[i].normal;
|
||||
|
||||
//is this surface flat enough to land on?
|
||||
if (currentNormal.y > minGroundNormalY)
|
||||
{
|
||||
IsGrounded = true;
|
||||
// if moving up, change the groundNormal to new surface normal.
|
||||
if (yMovement)
|
||||
{
|
||||
groundNormal = currentNormal;
|
||||
currentNormal.x = 0;
|
||||
}
|
||||
}
|
||||
if (IsGrounded)
|
||||
{
|
||||
//how much of our velocity aligns with surface normal?
|
||||
var projection = Vector2.Dot(velocity, currentNormal);
|
||||
if (projection < 0)
|
||||
{
|
||||
//slower velocity if moving against the normal (up a hill).
|
||||
velocity = velocity - projection * currentNormal;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//We are airborne, but hit something, so cancel vertical up and horizontal velocity.
|
||||
velocity.x *= 0;
|
||||
velocity.y = Mathf.Min(velocity.y, 0);
|
||||
}
|
||||
//remove shellDistance from actual move distance.
|
||||
var modifiedDistance = hitBuffer[i].distance - shellRadius;
|
||||
distance = modifiedDistance < distance ? modifiedDistance : distance;
|
||||
}
|
||||
}
|
||||
body.position = body.position + move.normalized * distance;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Platformer.Mechanics
|
||||
{
|
||||
public partial class PatrolPath
|
||||
{
|
||||
/// <summary>
|
||||
/// The Mover class oscillates between start and end points of a path at a defined speed.
|
||||
/// </summary>
|
||||
public class Mover
|
||||
{
|
||||
PatrolPath path;
|
||||
float p = 0;
|
||||
float duration;
|
||||
float startTime;
|
||||
|
||||
public Mover(PatrolPath path, float speed)
|
||||
{
|
||||
this.path = path;
|
||||
this.duration = (path.endPosition - path.startPosition).magnitude / speed;
|
||||
this.startTime = Time.time;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the position of the mover for the current frame.
|
||||
/// </summary>
|
||||
/// <value></value>
|
||||
public Vector2 Position
|
||||
{
|
||||
get
|
||||
{
|
||||
p = Mathf.InverseLerp(0, duration, Mathf.PingPong(Time.time - startTime, duration));
|
||||
return path.transform.TransformPoint(Vector2.Lerp(path.startPosition, path.endPosition, p));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Platformer.Mechanics
|
||||
{
|
||||
/// <summary>
|
||||
/// This component is used to create a patrol path, two points which enemies will move between.
|
||||
/// </summary>
|
||||
public partial class PatrolPath : MonoBehaviour
|
||||
{
|
||||
/// <summary>
|
||||
/// One end of the patrol path.
|
||||
/// </summary>
|
||||
public Vector2 startPosition, endPosition;
|
||||
|
||||
/// <summary>
|
||||
/// Create a Mover instance which is used to move an entity along the path at a certain speed.
|
||||
/// </summary>
|
||||
/// <param name="speed"></param>
|
||||
/// <returns></returns>
|
||||
public Mover CreateMover(float speed = 1) => new Mover(this, speed);
|
||||
|
||||
void Reset()
|
||||
{
|
||||
startPosition = Vector3.left;
|
||||
endPosition = Vector3.right;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// This class allows an audio clip to be played during an animation state.
|
||||
/// </summary>
|
||||
public class PlayAudioClip : StateMachineBehaviour
|
||||
{
|
||||
/// <summary>
|
||||
/// The point in normalized time where the clip should play.
|
||||
/// </summary>
|
||||
public float t = 0.5f;
|
||||
/// <summary>
|
||||
/// If greater than zero, the normalized time will be (normalizedTime % modulus).
|
||||
/// This is used to repeat the audio clip when the animation state loops.
|
||||
/// </summary>
|
||||
public float modulus = 0f;
|
||||
|
||||
/// <summary>
|
||||
/// The audio clip to be played.
|
||||
/// </summary>
|
||||
public AudioClip clip;
|
||||
float last_t = -1f;
|
||||
|
||||
override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
|
||||
{
|
||||
var nt = stateInfo.normalizedTime;
|
||||
if (modulus > 0f) nt %= modulus;
|
||||
if (nt >= t && last_t < t)
|
||||
AudioSource.PlayClipAtPoint(clip, animator.transform.position);
|
||||
last_t = nt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using Platformer.Gameplay;
|
||||
using static Platformer.Core.Simulation;
|
||||
using Platformer.Model;
|
||||
using Platformer.Core;
|
||||
using UnityEngine.InputSystem;
|
||||
|
||||
namespace Platformer.Mechanics
|
||||
{
|
||||
/// <summary>
|
||||
/// This is the main class used to implement control of the player.
|
||||
/// It is a superset of the AnimationController class, but is inlined to allow for any kind of customisation.
|
||||
/// </summary>
|
||||
public class PlayerController : KinematicObject
|
||||
{
|
||||
public AudioClip jumpAudio;
|
||||
public AudioClip respawnAudio;
|
||||
public AudioClip ouchAudio;
|
||||
|
||||
/// <summary>
|
||||
/// Max horizontal speed of the player.
|
||||
/// </summary>
|
||||
public float maxSpeed = 7;
|
||||
/// <summary>
|
||||
/// Initial jump velocity at the start of a jump.
|
||||
/// </summary>
|
||||
public float jumpTakeOffSpeed = 7;
|
||||
|
||||
public JumpState jumpState = JumpState.Grounded;
|
||||
private bool stopJump;
|
||||
/*internal new*/ public Collider2D collider2d;
|
||||
/*internal new*/ public AudioSource audioSource;
|
||||
public Health health;
|
||||
public bool controlEnabled = true;
|
||||
|
||||
bool jump;
|
||||
Vector2 move;
|
||||
SpriteRenderer spriteRenderer;
|
||||
internal Animator animator;
|
||||
readonly PlatformerModel model = Simulation.GetModel<PlatformerModel>();
|
||||
|
||||
private InputAction m_MoveAction;
|
||||
private InputAction m_JumpAction;
|
||||
|
||||
public Bounds Bounds => collider2d.bounds;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
health = GetComponent<Health>();
|
||||
audioSource = GetComponent<AudioSource>();
|
||||
collider2d = GetComponent<Collider2D>();
|
||||
spriteRenderer = GetComponent<SpriteRenderer>();
|
||||
animator = GetComponent<Animator>();
|
||||
|
||||
m_MoveAction = InputSystem.actions.FindAction("Player/Move");
|
||||
m_JumpAction = InputSystem.actions.FindAction("Player/Jump");
|
||||
|
||||
m_MoveAction.Enable();
|
||||
m_JumpAction.Enable();
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
if (controlEnabled)
|
||||
{
|
||||
move.x = m_MoveAction.ReadValue<Vector2>().x;
|
||||
if (jumpState == JumpState.Grounded && m_JumpAction.WasPressedThisFrame())
|
||||
jumpState = JumpState.PrepareToJump;
|
||||
else if (m_JumpAction.WasReleasedThisFrame())
|
||||
{
|
||||
stopJump = true;
|
||||
Schedule<PlayerStopJump>().player = this;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
move.x = 0;
|
||||
}
|
||||
UpdateJumpState();
|
||||
base.Update();
|
||||
}
|
||||
|
||||
void UpdateJumpState()
|
||||
{
|
||||
jump = false;
|
||||
switch (jumpState)
|
||||
{
|
||||
case JumpState.PrepareToJump:
|
||||
jumpState = JumpState.Jumping;
|
||||
jump = true;
|
||||
stopJump = false;
|
||||
break;
|
||||
case JumpState.Jumping:
|
||||
if (!IsGrounded)
|
||||
{
|
||||
Schedule<PlayerJumped>().player = this;
|
||||
jumpState = JumpState.InFlight;
|
||||
}
|
||||
break;
|
||||
case JumpState.InFlight:
|
||||
if (IsGrounded)
|
||||
{
|
||||
Schedule<PlayerLanded>().player = this;
|
||||
jumpState = JumpState.Landed;
|
||||
}
|
||||
break;
|
||||
case JumpState.Landed:
|
||||
jumpState = JumpState.Grounded;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void ComputeVelocity()
|
||||
{
|
||||
if (jump && IsGrounded)
|
||||
{
|
||||
velocity.y = jumpTakeOffSpeed * model.jumpModifier;
|
||||
jump = false;
|
||||
}
|
||||
else if (stopJump)
|
||||
{
|
||||
stopJump = false;
|
||||
if (velocity.y > 0)
|
||||
{
|
||||
velocity.y = velocity.y * model.jumpDeceleration;
|
||||
}
|
||||
}
|
||||
|
||||
if (move.x > 0.01f)
|
||||
spriteRenderer.flipX = false;
|
||||
else if (move.x < -0.01f)
|
||||
spriteRenderer.flipX = true;
|
||||
|
||||
animator.SetBool("grounded", IsGrounded);
|
||||
animator.SetFloat("velocityX", Mathf.Abs(velocity.x) / maxSpeed);
|
||||
|
||||
targetVelocity = move * maxSpeed;
|
||||
}
|
||||
|
||||
public enum JumpState
|
||||
{
|
||||
Grounded,
|
||||
PrepareToJump,
|
||||
Jumping,
|
||||
InFlight,
|
||||
Landed
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Platformer.Mechanics
|
||||
{
|
||||
/// <summary>
|
||||
/// Marks a gameobject as a spawnpoint in a scene.
|
||||
/// </summary>
|
||||
public class SpawnPoint : MonoBehaviour
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Platformer.Mechanics
|
||||
{
|
||||
/// <summary>
|
||||
/// This class animates all token instances in a scene.
|
||||
/// This allows a single update call to animate hundreds of sprite
|
||||
/// animations.
|
||||
/// If the tokens property is empty, it will automatically find and load
|
||||
/// all token instances in the scene at runtime.
|
||||
/// </summary>
|
||||
public class TokenController : MonoBehaviour
|
||||
{
|
||||
[Tooltip("Frames per second at which tokens are animated.")]
|
||||
public float frameRate = 12;
|
||||
[Tooltip("Instances of tokens which are animated. If empty, token instances are found and loaded at runtime.")]
|
||||
public TokenInstance[] tokens;
|
||||
|
||||
float nextFrameTime = 0;
|
||||
|
||||
[ContextMenu("Find All Tokens")]
|
||||
void FindAllTokensInScene()
|
||||
{
|
||||
tokens = UnityEngine.Object.FindObjectsByType<TokenInstance>(FindObjectsInactive.Exclude, FindObjectsSortMode.None);
|
||||
}
|
||||
|
||||
void Awake()
|
||||
{
|
||||
//if tokens are empty, find all instances.
|
||||
//if tokens are not empty, they've been added at editor time.
|
||||
if (tokens.Length == 0)
|
||||
FindAllTokensInScene();
|
||||
//Register all tokens so they can work with this controller.
|
||||
for (var i = 0; i < tokens.Length; i++)
|
||||
{
|
||||
tokens[i].tokenIndex = i;
|
||||
tokens[i].controller = this;
|
||||
}
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
//if it's time for the next frame...
|
||||
if (Time.time - nextFrameTime > (1f / frameRate))
|
||||
{
|
||||
//update all tokens with the next animation frame.
|
||||
for (var i = 0; i < tokens.Length; i++)
|
||||
{
|
||||
var token = tokens[i];
|
||||
//if token is null, it has been disabled and is no longer animated.
|
||||
if (token != null)
|
||||
{
|
||||
token._renderer.sprite = token.sprites[token.frame];
|
||||
if (token.collected && token.frame == token.sprites.Length - 1)
|
||||
{
|
||||
token.gameObject.SetActive(false);
|
||||
tokens[i] = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
token.frame = (token.frame + 1) % token.sprites.Length;
|
||||
}
|
||||
}
|
||||
}
|
||||
//calculate the time of the next frame.
|
||||
nextFrameTime += 1f / frameRate;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
using Platformer.Gameplay;
|
||||
using UnityEngine;
|
||||
using static Platformer.Core.Simulation;
|
||||
|
||||
|
||||
namespace Platformer.Mechanics
|
||||
{
|
||||
/// <summary>
|
||||
/// This class contains the data required for implementing token collection mechanics.
|
||||
/// It does not perform animation of the token, this is handled in a batch by the
|
||||
/// TokenController in the scene.
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(Collider2D))]
|
||||
public class TokenInstance : MonoBehaviour
|
||||
{
|
||||
public AudioClip tokenCollectAudio;
|
||||
[Tooltip("If true, animation will start at a random position in the sequence.")]
|
||||
public bool randomAnimationStartTime = false;
|
||||
[Tooltip("List of frames that make up the animation.")]
|
||||
public Sprite[] idleAnimation, collectedAnimation;
|
||||
|
||||
internal Sprite[] sprites = new Sprite[0];
|
||||
|
||||
internal SpriteRenderer _renderer;
|
||||
|
||||
//unique index which is assigned by the TokenController in a scene.
|
||||
internal int tokenIndex = -1;
|
||||
internal TokenController controller;
|
||||
//active frame in animation, updated by the controller.
|
||||
internal int frame = 0;
|
||||
internal bool collected = false;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
_renderer = GetComponent<SpriteRenderer>();
|
||||
if (randomAnimationStartTime)
|
||||
frame = Random.Range(0, sprites.Length);
|
||||
sprites = idleAnimation;
|
||||
}
|
||||
|
||||
void OnTriggerEnter2D(Collider2D other)
|
||||
{
|
||||
//only exectue OnPlayerEnter if the player collides with this token.
|
||||
var player = other.gameObject.GetComponent<PlayerController>();
|
||||
if (player != null) OnPlayerEnter(player);
|
||||
}
|
||||
|
||||
void OnPlayerEnter(PlayerController player)
|
||||
{
|
||||
if (collected) return;
|
||||
//disable the gameObject and remove it from the controller update list.
|
||||
frame = 0;
|
||||
sprites = collectedAnimation;
|
||||
if (controller != null)
|
||||
collected = true;
|
||||
//send an event into the gameplay system to perform some behaviour.
|
||||
var ev = Schedule<PlayerTokenCollision>();
|
||||
ev.token = this;
|
||||
ev.player = player;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using Platformer.Gameplay;
|
||||
using UnityEngine;
|
||||
using static Platformer.Core.Simulation;
|
||||
|
||||
namespace Platformer.Mechanics
|
||||
{
|
||||
/// <summary>
|
||||
/// Marks a trigger as a VictoryZone, usually used to end the current game level.
|
||||
/// </summary>
|
||||
public class VictoryZone : MonoBehaviour
|
||||
{
|
||||
void OnTriggerEnter2D(Collider2D collider)
|
||||
{
|
||||
var p = collider.gameObject.GetComponent<PlayerController>();
|
||||
if (p != null)
|
||||
{
|
||||
var ev = Schedule<PlayerEnteredVictoryZone>();
|
||||
ev.victoryZone = this;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using Platformer.Mechanics;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Platformer.Model
|
||||
{
|
||||
/// <summary>
|
||||
/// The main model containing needed data to implement a platformer style
|
||||
/// game. This class should only contain data, and methods that operate
|
||||
/// on the data. It is initialised with data in the GameController class.
|
||||
/// </summary>
|
||||
[System.Serializable]
|
||||
public class PlatformerModel
|
||||
{
|
||||
/// <summary>
|
||||
/// The virtual camera in the scene.
|
||||
/// </summary>
|
||||
public Unity.Cinemachine.CinemachineCamera virtualCamera;
|
||||
|
||||
/// <summary>
|
||||
/// The main component which controls the player sprite, controlled
|
||||
/// by the user.
|
||||
/// </summary>
|
||||
public PlayerController player;
|
||||
|
||||
/// <summary>
|
||||
/// The spawn point in the scene.
|
||||
/// </summary>
|
||||
public Transform spawnPoint;
|
||||
|
||||
/// <summary>
|
||||
/// A global jump modifier applied to all initial jump velocities.
|
||||
/// </summary>
|
||||
public float jumpModifier = 1.5f;
|
||||
|
||||
/// <summary>
|
||||
/// A global jump modifier applied to slow down an active jump when
|
||||
/// the user releases the jump input.
|
||||
/// </summary>
|
||||
public float jumpDeceleration = 0.5f;
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Platformer.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// A simple controller for switching between UI panels.
|
||||
/// </summary>
|
||||
public class MainUIController : MonoBehaviour
|
||||
{
|
||||
public GameObject[] panels;
|
||||
|
||||
public void SetActivePanel(int index)
|
||||
{
|
||||
for (var i = 0; i < panels.Length; i++)
|
||||
{
|
||||
var active = i == index;
|
||||
var g = panels[i];
|
||||
if (g.activeSelf != active) g.SetActive(active);
|
||||
}
|
||||
}
|
||||
|
||||
void OnEnable()
|
||||
{
|
||||
SetActivePanel(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
using Platformer.Mechanics;
|
||||
using Platformer.UI;
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
|
||||
namespace Platformer.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// The MetaGameController is responsible for switching control between the high level
|
||||
/// contexts of the application, eg the Main Menu and Gameplay systems.
|
||||
/// </summary>
|
||||
public class MetaGameController : MonoBehaviour
|
||||
{
|
||||
/// <summary>
|
||||
/// The main UI object which used for the menu.
|
||||
/// </summary>
|
||||
public MainUIController mainMenu;
|
||||
|
||||
/// <summary>
|
||||
/// A list of canvas objects which are used during gameplay (when the main ui is turned off)
|
||||
/// </summary>
|
||||
public Canvas[] gamePlayCanvasii;
|
||||
|
||||
/// <summary>
|
||||
/// The game controller.
|
||||
/// </summary>
|
||||
public GameController gameController;
|
||||
|
||||
bool showMainCanvas = false;
|
||||
private InputAction m_MenuAction;
|
||||
|
||||
void OnEnable()
|
||||
{
|
||||
_ToggleMainMenu(showMainCanvas);
|
||||
m_MenuAction = InputSystem.actions.FindAction("Player/Menu");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Turn the main menu on or off.
|
||||
/// </summary>
|
||||
/// <param name="show"></param>
|
||||
public void ToggleMainMenu(bool show)
|
||||
{
|
||||
if (this.showMainCanvas != show)
|
||||
{
|
||||
_ToggleMainMenu(show);
|
||||
}
|
||||
}
|
||||
|
||||
void _ToggleMainMenu(bool show)
|
||||
{
|
||||
if (show)
|
||||
{
|
||||
Time.timeScale = 0;
|
||||
mainMenu.gameObject.SetActive(true);
|
||||
foreach (var i in gamePlayCanvasii) i.gameObject.SetActive(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
Time.timeScale = 1;
|
||||
mainMenu.gameObject.SetActive(false);
|
||||
foreach (var i in gamePlayCanvasii) i.gameObject.SetActive(true);
|
||||
}
|
||||
this.showMainCanvas = show;
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (m_MenuAction.WasPressedThisFrame())
|
||||
{
|
||||
ToggleMainMenu(show: !showMainCanvas);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor;
|
||||
#endif
|
||||
|
||||
using UnityEngine;
|
||||
using UnityEngine.Tilemaps;
|
||||
|
||||
namespace Platformer.View
|
||||
{
|
||||
[System.Serializable]
|
||||
[CreateAssetMenu(fileName = "New Animated Tile", menuName = "Tiles/Animated Tile")]
|
||||
public class AnimatedTile : TileBase
|
||||
{
|
||||
public Sprite[] m_AnimatedSprites;
|
||||
public float m_MinSpeed = 1f;
|
||||
public float m_MaxSpeed = 1f;
|
||||
public float m_AnimationStartTime;
|
||||
public Tile.ColliderType m_TileColliderType;
|
||||
|
||||
public override void GetTileData(Vector3Int location, ITilemap tileMap, ref TileData tileData)
|
||||
{
|
||||
tileData.transform = Matrix4x4.identity;
|
||||
tileData.color = Color.white;
|
||||
if (m_AnimatedSprites != null && m_AnimatedSprites.Length > 0)
|
||||
{
|
||||
tileData.sprite = m_AnimatedSprites[m_AnimatedSprites.Length - 1];
|
||||
tileData.colliderType = m_TileColliderType;
|
||||
}
|
||||
}
|
||||
|
||||
public override bool GetTileAnimationData(Vector3Int location, ITilemap tileMap, ref TileAnimationData tileAnimationData)
|
||||
{
|
||||
if (m_AnimatedSprites.Length > 0)
|
||||
{
|
||||
tileAnimationData.animatedSprites = m_AnimatedSprites;
|
||||
tileAnimationData.animationSpeed = Random.Range(m_MinSpeed, m_MaxSpeed);
|
||||
tileAnimationData.animationStartTime = m_AnimationStartTime;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
[CustomEditor(typeof(AnimatedTile))]
|
||||
public class AnimatedTileEditor : Editor
|
||||
{
|
||||
private AnimatedTile tile { get { return (target as AnimatedTile); } }
|
||||
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
EditorGUI.BeginChangeCheck();
|
||||
int count = EditorGUILayout.DelayedIntField("Number of Animated Sprites", tile.m_AnimatedSprites != null ? tile.m_AnimatedSprites.Length : 0);
|
||||
if (count < 0)
|
||||
count = 0;
|
||||
|
||||
if (tile.m_AnimatedSprites == null || tile.m_AnimatedSprites.Length != count)
|
||||
{
|
||||
System.Array.Resize<Sprite>(ref tile.m_AnimatedSprites, count);
|
||||
}
|
||||
|
||||
if (count == 0)
|
||||
return;
|
||||
|
||||
EditorGUILayout.LabelField("Place sprites shown based on the order of animation.");
|
||||
EditorGUILayout.Space();
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
tile.m_AnimatedSprites[i] = (Sprite)EditorGUILayout.ObjectField("Sprite " + (i + 1), tile.m_AnimatedSprites[i], typeof(Sprite), false, null);
|
||||
}
|
||||
|
||||
float minSpeed = EditorGUILayout.FloatField("Minimum Speed", tile.m_MinSpeed);
|
||||
float maxSpeed = EditorGUILayout.FloatField("Maximum Speed", tile.m_MaxSpeed);
|
||||
if (minSpeed < 0.0f)
|
||||
minSpeed = 0.0f;
|
||||
|
||||
if (maxSpeed < 0.0f)
|
||||
maxSpeed = 0.0f;
|
||||
|
||||
if (maxSpeed < minSpeed)
|
||||
maxSpeed = minSpeed;
|
||||
|
||||
tile.m_MinSpeed = minSpeed;
|
||||
tile.m_MaxSpeed = maxSpeed;
|
||||
|
||||
tile.m_AnimationStartTime = EditorGUILayout.FloatField("Start Time", tile.m_AnimationStartTime);
|
||||
tile.m_TileColliderType = (Tile.ColliderType)EditorGUILayout.EnumPopup("Collider Type", tile.m_TileColliderType);
|
||||
if (EditorGUI.EndChangeCheck())
|
||||
EditorUtility.SetDirty(tile);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Platformer.View
|
||||
{
|
||||
/// <summary>
|
||||
/// Used to move a transform relative to the main camera position with a scale factor applied.
|
||||
/// This is used to implement parallax scrolling effects on different branches of gameobjects.
|
||||
/// </summary>
|
||||
public class ParallaxLayer : MonoBehaviour
|
||||
{
|
||||
/// <summary>
|
||||
/// Movement of the layer is scaled by this value.
|
||||
/// </summary>
|
||||
public Vector3 movementScale = Vector3.one;
|
||||
|
||||
Transform _camera;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
_camera = Camera.main.transform;
|
||||
}
|
||||
|
||||
void LateUpdate()
|
||||
{
|
||||
transform.position = Vector3.Scale(_camera.position, movementScale);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user