Neues Projekt

This commit is contained in:
2026-05-20 19:47:36 +02:00
parent 01d76eccb1
commit 21aeb533c9
699 changed files with 19934 additions and 427294 deletions
+31
View File
@@ -0,0 +1,31 @@
using UnityEngine;
public class CameraController : MonoBehaviour
{
//Room camera
[SerializeField] private float speed;
private float currentPosX;
private Vector3 velocity = Vector3.zero;
//Follow player
[SerializeField] private Transform player;
[SerializeField] private float aheadDistance;
[SerializeField] private float cameraSpeed;
private float lookAhead;
private void Update()
{
//Room camera
//transform.position = Vector3.SmoothDamp(transform.position, new Vector3(currentPosX, transform.position.y, transform.position.z), ref velocity, speed);
//Follow player
transform.position = new Vector3(player.position.x + lookAhead, player.position.y, transform.position.z);
lookAhead = Mathf.Lerp(lookAhead, (aheadDistance * player.localScale.x), Time.deltaTime * cameraSpeed);
}
public void MoveToNewRoom(Transform _newRoom)
{
print("here");
currentPosX = _newRoom.position.x;
}
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: aca0f06ef35117949a91eeca9a4652ad
+335
View File
@@ -0,0 +1,335 @@
using UnityEngine;
[RequireComponent(typeof(Rigidbody2D))]
public class CelesteStyleController2D : MonoBehaviour
{
[Header("References")]
[SerializeField] private Rigidbody2D body;
[SerializeField] private Collider2D col;
[SerializeField] private Transform groundCheck;
[SerializeField] private Transform wallCheck;
[SerializeField] private LayerMask groundLayer;
[Header("Run")]
[SerializeField] private float maxRunSpeed = 9f;
[SerializeField] private float runAcceleration = 70f;
[SerializeField] private float runDeceleration = 80f;
[SerializeField] private float airAccelerationMultiplier = 0.65f;
[Header("Jump")]
[SerializeField] private float jumpVelocity = 14f;
[SerializeField] private float coyoteTime = 0.12f;
[SerializeField] private float jumpBufferTime = 0.12f;
[SerializeField] private float jumpCutMultiplier = 0.5f; // wenn Jump losgelassen wird
[Header("Gravity Feel")]
[SerializeField] private float fallGravityMultiplier = 1.6f; // schneller fallen
[SerializeField] private float lowJumpGravityMultiplier = 1.3f; // wenn Jump nicht gehalten wird
[Header("Checks")]
[SerializeField] private Vector2 groundCheckSize = new Vector2(0.6f, 0.12f);
[SerializeField] private float wallCheckDistance = 0.35f;
[Header("Wall Slide & Wall Jump")]
[SerializeField] private bool enableWallSlide = true;
[SerializeField] private float wallSlideMaxSpeed = 3.5f;
[SerializeField] private float wallJumpX = 10f;
[SerializeField] private float wallJumpY = 14f;
[SerializeField] private float wallJumpInputLock = 0.15f; // kurz kein normaler Move nach Walljump
[SerializeField] private bool requireMoveIntoWallForSlide = true; // Celeste-like: zum Sliden Richtung Wand halten
[Header("Dash (optional)")]
[SerializeField] private bool enableDash = true;
[SerializeField] private float dashSpeed = 18f;
[SerializeField] private float dashTime = 0.16f;
[SerializeField] private float dashCooldown = 0.05f; // kleine Pause nach Dash
[SerializeField] private int airDashes = 1;
// input
private float moveX;
private bool jumpPressed;
private bool jumpHeld;
private bool dashPressed;
// state
private bool grounded;
private bool onWallLeft;
private bool onWallRight;
private bool wallSliding;
private int wallDir; // -1 links, +1 rechts
private float coyoteCounter;
private float jumpBufferCounter;
private float inputLockCounter;
// dash state
private bool isDashing;
private float dashCounter;
private float dashCooldownCounter;
private int dashesLeft;
private float baseGravity;
private int facingDir = 1; // 1 = rechts, -1 = links
private void Reset()
{
body = GetComponent<Rigidbody2D>();
col = GetComponent<Collider2D>();
}
private void Awake()
{
if (!body) body = GetComponent<Rigidbody2D>();
if (!col) col = GetComponent<Collider2D>();
baseGravity = body.gravityScale;
dashesLeft = airDashes;
}
private void Update()
{
ReadInput();
UpdateChecks();
UpdateTimers();
HandleFlip();
// Jump buffer aktualisieren
if (jumpPressed) jumpBufferCounter = jumpBufferTime;
// Dash
if (enableDash && dashPressed && !isDashing && dashCooldownCounter <= 0f && dashesLeft > 0)
{
StartDash();
}
}
private void FixedUpdate()
{
if (isDashing)
{
dashCounter -= Time.fixedDeltaTime;
if (dashCounter <= 0f) EndDash();
return;
}
if (dashCooldownCounter > 0f)
dashCooldownCounter -= Time.fixedDeltaTime;
ApplyHorizontalMovement();
ApplyWallSlide();
TryConsumeBufferedJump();
ApplyBetterGravity();
}
// ---------------- INPUT & CHECKS ----------------
private void ReadInput()
{
moveX = Input.GetAxisRaw("Horizontal");
jumpPressed = Input.GetButtonDown("Jump");
jumpHeld = Input.GetButton("Jump");
dashPressed = Input.GetKeyDown(KeyCode.LeftShift) || Input.GetKeyDown(KeyCode.X); // frei anpassbar
}
private void UpdateChecks()
{
// Ground check (OverlapBox)
grounded = Physics2D.OverlapBox(groundCheck.position, groundCheckSize, 0f, groundLayer);
// Wall check (Raycasts)
onWallLeft = Physics2D.Raycast(wallCheck.position, Vector2.left, wallCheckDistance, groundLayer);
onWallRight = Physics2D.Raycast(wallCheck.position, Vector2.right, wallCheckDistance, groundLayer);
wallDir = onWallRight ? 1 : (onWallLeft ? -1 : 0);
// Dash reset am Boden
if (grounded)
dashesLeft = airDashes;
}
private void UpdateTimers()
{
// Coyote Time
if (grounded) coyoteCounter = coyoteTime;
else coyoteCounter -= Time.deltaTime;
// Jump Buffer
if (jumpBufferCounter > 0f) jumpBufferCounter -= Time.deltaTime;
// Input Lock nach Walljump
if (inputLockCounter > 0f) inputLockCounter -= Time.deltaTime;
}
private void HandleFlip()
{
// Facing nur wenn Spieler wirklich luft
if (moveX != 0 && !isDashing)
{
facingDir = (moveX > 0) ? 1 : -1;
// Simple Flip: Scale X (alternativ SpriteRenderer.flipX)
Vector3 s = transform.localScale;
s.x = Mathf.Abs(s.x) * facingDir;
transform.localScale = s;
}
}
// ---------------- MOVEMENT ----------------
private void ApplyHorizontalMovement()
{
if (inputLockCounter > 0f) return;
float targetSpeed = moveX * maxRunSpeed;
float accel = (Mathf.Abs(targetSpeed) > 0.01f) ? runAcceleration : runDeceleration;
if (!grounded) accel *= airAccelerationMultiplier;
float speedDiff = targetSpeed - body.linearVelocity.x;
float movement = speedDiff * accel;
body.AddForce(Vector2.right * movement);
// Clamp, damit du nicht "durch AddForce" zu schnell wirst
float clampedX = Mathf.Clamp(body.linearVelocity.x, -maxRunSpeed, maxRunSpeed);
body.linearVelocity = new Vector2(clampedX, body.linearVelocity.y);
}
// ---------------- JUMP LOGIC ----------------
private void TryConsumeBufferedJump()
{
if (jumpBufferCounter <= 0f) return;
// Prioritt: Wenn wallSliding ? Walljump, sonst normal Jump (Coyote)
if (wallSliding)
{
DoWallJump();
jumpBufferCounter = 0f;
return;
}
if (coyoteCounter > 0f)
{
DoJump();
jumpBufferCounter = 0f;
}
}
private void DoJump()
{
// konsistent: setze Y-Velocity (Celeste-like)
body.linearVelocity = new Vector2(body.linearVelocity.x, jumpVelocity);
coyoteCounter = 0f;
}
private void DoWallJump()
{
// weg von Wand: wenn Wand rechts (+1), spring nach links (-1)
Vector2 v = new Vector2(-wallDir * wallJumpX, wallJumpY);
body.linearVelocity = Vector2.zero;
body.linearVelocity = v;
// kurzer Lock gegen Input "zieht dich zurck"
inputLockCounter = wallJumpInputLock;
// Nach Walljump gilt Sprung als verbraucht, aber du kannst dashen
coyoteCounter = 0f;
}
// ---------------- WALL SLIDE ----------------
private void ApplyWallSlide()
{
wallSliding = false;
if (!enableWallSlide) return;
if (grounded) return;
if (wallDir == 0) return;
// Optional: nur sliden, wenn Richtung zur Wand gedrckt wird
if (requireMoveIntoWallForSlide)
{
if (moveX == 0) return;
if (Mathf.Sign(moveX) != wallDir) return;
}
// Nur sliden wenn man nach unten fllt (Celeste-feel)
if (body.linearVelocity.y < 0f)
{
wallSliding = true;
float newY = Mathf.Max(body.linearVelocity.y, -wallSlideMaxSpeed);
body.linearVelocity = new Vector2(body.linearVelocity.x, newY);
}
}
// ---------------- GRAVITY FEEL ----------------
private void ApplyBetterGravity()
{
// Wenn man fllt: schneller
if (body.linearVelocity.y < 0f)
{
body.linearVelocity += Vector2.up * Physics2D.gravity.y * (fallGravityMultiplier - 1f) * Time.fixedDeltaTime;
}
// Wenn man aufsteigt, aber Jump nicht mehr gehalten wird: Jump "cut"
else if (body.linearVelocity.y > 0f && !jumpHeld)
{
body.linearVelocity += Vector2.up * Physics2D.gravity.y * (lowJumpGravityMultiplier - 1f) * Time.fixedDeltaTime;
}
// Optional: sofortiges jump cut beim Loslassen (noch snappier)
if (!jumpHeld && body.linearVelocity.y > 0f)
{
body.linearVelocity = new Vector2(body.linearVelocity.x, body.linearVelocity.y * (1f - (1f - jumpCutMultiplier) * 0.02f));
}
}
// ---------------- DASH ----------------
private void StartDash()
{
isDashing = true;
dashCounter = dashTime;
dashCooldownCounter = 0f;
dashesLeft--;
// Dash direction: Input, sonst facing
Vector2 dir = new Vector2(Input.GetAxisRaw("Horizontal"), Input.GetAxisRaw("Vertical"));
if (dir.sqrMagnitude < 0.01f) dir = new Vector2(facingDir, 0f);
dir.Normalize();
// whrend Dash keine Gravity
body.gravityScale = 0f;
body.linearVelocity = dir * dashSpeed;
}
private void EndDash()
{
isDashing = false;
dashCooldownCounter = dashCooldown;
body.gravityScale = baseGravity;
// Optional: kleiner "carry", damit dash nicht abrupt stoppt
body.linearVelocity = new Vector2(body.linearVelocity.x * 0.9f, body.linearVelocity.y);
}
// ---------------- GIZMOS ----------------
private void OnDrawGizmosSelected()
{
if (groundCheck)
{
Gizmos.color = Color.green;
Gizmos.DrawWireCube(groundCheck.position, groundCheckSize);
}
if (wallCheck)
{
Gizmos.color = Color.cyan;
Gizmos.DrawLine(wallCheck.position, wallCheck.position + Vector3.left * wallCheckDistance);
Gizmos.DrawLine(wallCheck.position, wallCheck.position + Vector3.right * wallCheckDistance);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 2427fe66151a6c34b8db3ee220f4574e
-32
View File
@@ -1,32 +0,0 @@
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);
}
}
}
-112
View File
@@ -1,112 +0,0 @@
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);
}
}
}
-63
View File
@@ -1,63 +0,0 @@
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);
}
}
}
}
}
@@ -1,16 +0,0 @@
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();
}
}
}
-140
View File
@@ -1,140 +0,0 @@
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;
}
}
}
+51
View File
@@ -0,0 +1,51 @@
using UnityEngine;
public class EnemyHealth : MonoBehaviour
{
[SerializeField] private int startingHealth = 3;
private int currentHealth;
private Animator anim;
private bool dead;
private void Awake()
{
currentHealth = startingHealth;
anim = GetComponent<Animator>();
}
public void TakeDamage(int damage)
{
if (dead) return;
currentHealth -= damage;
anim.SetTrigger("hurt");
if (currentHealth <= 0)
{
Die();
Debug.Log("Enemy is down");
}
}
private void Die()
{
dead = true;
anim.SetTrigger("die");
// Enemy deaktivieren
GetComponent<MeleeEnemy>().enabled = false;
// Collider ausschalten
GetComponent<Collider2D>().enabled = false;
// Optional: Rigidbody stoppen
Rigidbody2D rb = GetComponent<Rigidbody2D>();
if (rb != null)
rb.linearVelocity = Vector2.zero;
Destroy(gameObject, 1.5f);
}
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 1048070f5c7f1444b9ea85c82ad3dc21
+73
View File
@@ -0,0 +1,73 @@
using UnityEngine;
public class EnemyPatrol : MonoBehaviour
{
[Header ("Patrol Points")]
[SerializeField] private Transform leftEdge;
[SerializeField] private Transform rightEdge;
[Header("Enemy")]
[SerializeField] private Transform enemy;
[Header("Movement parameters")]
[SerializeField] private float speed;
private Vector3 initScale;
private bool movingLeft;
[Header("Idle Behaviour")]
[SerializeField] private float idleDuration;
private float idleTimer;
[Header("Enemy Animator")]
[SerializeField] private Animator anim;
private void Awake()
{
initScale = enemy.localScale;
}
private void OnDisable()
{
anim.SetBool("moving", false);
}
private void Update()
{
if (movingLeft)
{
if (enemy.position.x >= leftEdge.position.x)
MoveInDirection(-1);
else
DirectionChange();
}
else
{
if (enemy.position.x <= rightEdge.position.x)
MoveInDirection(1);
else
DirectionChange();
}
}
private void DirectionChange()
{
anim.SetBool("moving", false);
idleTimer += Time.deltaTime;
if(idleTimer > idleDuration)
movingLeft = !movingLeft;
}
private void MoveInDirection(int _direction)
{
idleTimer = 0;
anim.SetBool("moving", true);
//Make enemy face direction
enemy.localScale = new Vector3(Mathf.Abs(initScale.x) * _direction,
initScale.y, initScale.z);
//Move in that direction
enemy.position = new Vector3(enemy.position.x + Time.deltaTime * _direction * speed,
enemy.position.y, enemy.position.z);
}
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 0c5eb5496676f4c449527ac55342b9b6
@@ -1,19 +0,0 @@
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;
}
}
}
-22
View File
@@ -1,22 +0,0 @@
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);
}
}
}
-21
View File
@@ -1,21 +0,0 @@
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>();
}
}
}
-36
View File
@@ -1,36 +0,0 @@
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);
}
}
}
}
@@ -1,53 +0,0 @@
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>();
}
}
}
}
@@ -1,22 +0,0 @@
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);
}
}
}
@@ -1,24 +0,0 @@
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;
}
}
}
-20
View File
@@ -1,20 +0,0 @@
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);
}
}
}
-19
View File
@@ -1,19 +0,0 @@
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()
{
}
}
}
-30
View File
@@ -1,30 +0,0 @@
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);
}
}
}
-19
View File
@@ -1,19 +0,0 @@
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()
{
}
}
}
@@ -1,24 +0,0 @@
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);
}
}
}
+34
View File
@@ -0,0 +1,34 @@
using UnityEngine;
using System.Collections;
public class Health : MonoBehaviour
{
[Header ("Health")]
[SerializeField] private float startingHealth;
public float currentHealth { get; private set; }
private bool dead;
void Start()
{
currentHealth = startingHealth;
}
public void TakeDamage(float damage)
{
if (currentHealth > 0)
{
currentHealth -= damage;
Debug.Log($"{this.gameObject.name} was hit. Health: {currentHealth}");
}
else
{
Debug.Log($"{this.gameObject.name} is Dead");
dead = true;
}
}
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: c6cbe560ee78d0c4a8d9abbb8cab93d5
@@ -1,76 +0,0 @@
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;
}
}
}
-25
View File
@@ -1,25 +0,0 @@
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;
}
}
}
}
@@ -1,55 +0,0 @@
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);
}
}
}
}
@@ -1,38 +0,0 @@
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();
}
}
}
-60
View File
@@ -1,60 +0,0 @@
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;
}
}
}
-176
View File
@@ -1,176 +0,0 @@
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;
}
}
}
@@ -1,38 +0,0 @@
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));
}
}
}
}
}
-28
View File
@@ -1,28 +0,0 @@
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;
}
}
}
-34
View File
@@ -1,34 +0,0 @@
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;
}
}
@@ -1,151 +0,0 @@
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
}
}
}
-12
View File
@@ -1,12 +0,0 @@
using UnityEngine;
namespace Platformer.Mechanics
{
/// <summary>
/// Marks a gameobject as a spawnpoint in a scene.
/// </summary>
public class SpawnPoint : MonoBehaviour
{
}
}
@@ -1,71 +0,0 @@
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;
}
}
}
}
-62
View File
@@ -1,62 +0,0 @@
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;
}
}
}
-22
View File
@@ -1,22 +0,0 @@
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;
}
}
}
}
+76
View File
@@ -0,0 +1,76 @@
using UnityEngine;
public class MeleeEnemy : MonoBehaviour
{
[Header("Attack Parameters")]
[SerializeField] private float attackCooldown;
[SerializeField] private float range;
[SerializeField] private int damage;
[Header("Collider Parameters")]
[SerializeField] private float colliderDistance;
[SerializeField] private BoxCollider2D boxCollider;
[Header("Player Layer")]
[SerializeField] private LayerMask playerLayer;
private float cooldownTimer = Mathf.Infinity;
//References
private Animator anim;
private Health playerHealth;
private EnemyPatrol enemyPatrol;
private void Awake()
{
anim = GetComponent<Animator>();
enemyPatrol = GetComponentInParent<EnemyPatrol>();
}
private void Update()
{
cooldownTimer += Time.deltaTime;
//Attack only when player in sight?
if (PlayerInSight())
{
if (cooldownTimer >= attackCooldown)
{
cooldownTimer = 0;
anim.SetTrigger("meleeAttack");
DamagePlayer();
}
}
if (enemyPatrol != null)
enemyPatrol.enabled = !PlayerInSight();
}
private bool PlayerInSight()
{
RaycastHit2D hit =
Physics2D.BoxCast(boxCollider.bounds.center + transform.right * range * transform.localScale.x * colliderDistance,
new Vector3(boxCollider.bounds.size.x * range, boxCollider.bounds.size.y, boxCollider.bounds.size.z),
0, Vector2.left, 0, playerLayer);
if (hit.collider != null)
{
playerHealth = hit.transform.GetComponent<Health>();
Debug.Log("Player hit");
}
return hit.collider != null;
}
private void OnDrawGizmos()
{
Gizmos.color = Color.red;
Gizmos.DrawWireCube(boxCollider.bounds.center + transform.right * range * transform.localScale.x * colliderDistance,
new Vector3(boxCollider.bounds.size.x * range, boxCollider.bounds.size.y, boxCollider.bounds.size.z));
}
private void DamagePlayer()
{
if (PlayerInSight())
playerHealth.TakeDamage(damage);
}
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 5badd1682eee4c24fa71895cdad30a6a
-42
View File
@@ -1,42 +0,0 @@
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;
}
}
+76
View File
@@ -0,0 +1,76 @@
using UnityEngine;
using UnityEngine.InputSystem;
public class PlayerCombat : MonoBehaviour
{
[Header("Attack Parameters")]
[SerializeField] private float attackCooldown;
[SerializeField] private float range;
[SerializeField] private int damage;
[Header("Collider Parameters")]
[SerializeField] private float colliderDistance;
[SerializeField] private BoxCollider2D boxCollider;
[Header("Enemy Layer")]
[SerializeField] private LayerMask enemyLayer;
private float cooldownTimer = 0f;
//References
private Animator anim;
void Start()
{
anim = GetComponent<Animator>();
}
void Update()
{
cooldownTimer += Time.deltaTime;
}
public void Melee(InputAction.CallbackContext callback)
{
if (callback.performed && cooldownTimer >= attackCooldown)
{
Debug.Log("Melee Attack");
RaycastHit2D hit =
Physics2D.BoxCast(
boxCollider.bounds.center + transform.right * range * transform.localScale.x * colliderDistance,
new Vector3(boxCollider.bounds.size.x * range, boxCollider.bounds.size.y, boxCollider.bounds.size.z),
0,
Vector2.left,
0,
enemyLayer);
anim.SetTrigger("meleeAttack");
cooldownTimer = 0f;
if (hit.collider != null)
{
Debug.Log("Hit");
EnemyHealth enemyHealth =
hit.transform.GetComponent<EnemyHealth>();
if (enemyHealth != null)
{
enemyHealth.TakeDamage(damage);
}
}
}
}
private void OnDrawGizmos()
{
Gizmos.color = Color.red;
Gizmos.DrawWireCube(
boxCollider.bounds.center + transform.right * range * transform.localScale.x * colliderDistance,
new Vector3(boxCollider.bounds.size.x * range,
boxCollider.bounds.size.y,
boxCollider.bounds.size.z));
}
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 0570a97731b7eab49953751de893f523
+215
View File
@@ -0,0 +1,215 @@
using UnityEngine;
using UnityEngine.InputSystem;
public class PlayerMovement : MonoBehaviour
{
// Start is called once before the first execution of Update after the MonoBehaviour is created
[SerializeField] private float speed = 10f;
[SerializeField] private float jumpPower = 1.4f;
private float wallJumpX = 1500; //Horizontal wall jump force
private float wallJumpY = 750; //Vertical wall jump force
[Header("Wallmovement")]
[SerializeField] private float coyoteTime; //How much time the player can hang in the air before jumping
private float coyoteCounter; //How much time passed since the player ran off the edge
public float wallSlideSpeed = 2;
public Vector2 wallJumpPower = new Vector2(10f, 10f);
public Vector2 wallClimbPower = new Vector2(5f, 10f);
[Header("Layers")]
[SerializeField] private LayerMask groundLayer;
[SerializeField] private LayerMask wallLayer;
private Rigidbody2D body;
private Animator animator;
private BoxCollider2D boxCollider2D;
//private bool grounded;
private Vector2 moveInput;
private float wallJumpCooldown;
//Wall Jumping
//bool isWallJumping;
//float wallJumpDirection;
//float wallJumpTime = 0.5f;
//float wallJumpTimer;
//private float wallJumpLockTime = 0.15f;
//private float wallJumpLockCounter;
private void Awake()
{
}
void Start()
{
body = GetComponent<Rigidbody2D>();
animator = GetComponent<Animator>();
boxCollider2D = GetComponent<BoxCollider2D>();
}
private void Update()
{
}
// Update is called once per frame
private void FixedUpdate()
{
float horizontalInput = moveInput.x;
//body.linearVelocity = new Vector2(moveInput.x * speed, body.linearVelocityY);
//Flip player right/left movement
if (horizontalInput > 0.01f)
{
transform.localScale = Vector3.one;
} else if (horizontalInput < -0.01f)
{
transform.localScale = new Vector3(-1, 1, 1);
}
if (OnWall())
{
if (moveInput.x != 0)
{
//body.gravityScale = 0;
//body.linearVelocity = Vector2.zero;
WallSlide();
}
}
else
{
//body.gravityScale = 7;
if (moveInput.x != 0)
body.linearVelocity = new Vector2(horizontalInput * speed, body.linearVelocity.y);
//else
//{
// body.linearVelocity = new Vector2(horizontalInput * speed, body.linearVelocity.y);
//}
if (IsGrounded())
{
coyoteCounter = coyoteTime; //Reset coyote counter when on the ground
body.linearVelocity = new Vector2(moveInput.x * speed, body.linearVelocityY);
}
else
coyoteCounter -= Time.deltaTime; //Start decreasing coyote counter when not on the ground
}
//Set abunatir parameters
animator.SetBool("run", horizontalInput != 0);
animator.SetBool("grounded", IsGrounded());
}
public void Jump(InputAction.CallbackContext callback)
{
if (coyoteCounter <= 0 && !OnWall() ) return;
//Debug.Log($"Jumping {callback.performed} - Is Grounded {IsGrounded()}");
//if (callback.performed && IsGrounded())
if (callback.performed)
{
if (OnWall() && !IsGrounded())
{
if (moveInput.x == 0)
{
WallJump();
} else
{
WallClimb();
}
}
else
{
if (IsGrounded())
{
Debug.Log("We are supposed to jump");
body.linearVelocity = new Vector2(body.linearVelocityX, speed * jumpPower);
animator.SetTrigger("jump");
}
else
{
if (coyoteCounter > 0)
body.linearVelocity = new Vector2(body.linearVelocity.x, jumpPower);
}
//Reset coyote counter to 0 to avoid double jumps
coyoteCounter = 0;
}
}
}
public void Move(InputAction.CallbackContext callback)
{
moveInput = callback.ReadValue<Vector2>();
//Debug.Log("Move Input " + moveInput);
}
private void OnCollisionEnter2D(Collision2D collision)
{
//if(collision.gameObject.tag == "Ground")
//{
// grounded = true;
//}
}
private void WallSlide()
{
//if (OnWall() )
//{
// body.linearVelocity = new Vector2(body.linearVelocityX, Mathf.Max(body.linearVelocity.y, -wallSlideSpeed));
//}
Debug.Log("Wallsliding");
body.linearVelocity = new Vector2(body.linearVelocityX, Mathf.Max(body.linearVelocity.y, -wallSlideSpeed));
}
private void WallJump()
{
//Climb
//body.AddForce(new Vector2(-Mathf.Sign(transform.localScale.x) * wallJumpX, wallJumpY), ForceMode2D.Impulse);
Debug.Log("WallJump");
//Jump
body.linearVelocity = new Vector2(-Mathf.Sign(transform.localScale.x) * wallJumpPower.x, wallJumpPower.y);
wallJumpCooldown = 0;
}
private void WallClimb()
{
//Climb
//body.AddForce(new Vector2(-Mathf.Sign(transform.localScale.x) * wallJumpX, wallJumpY));
Debug.Log("WallClimb");
//Jump
body.linearVelocity = new Vector2(-Mathf.Sign(transform.localScale.x) * wallClimbPower.x, wallClimbPower.y);
wallJumpCooldown = 0;
}
private bool IsGrounded()
{
RaycastHit2D hit = Physics2D.BoxCast(boxCollider2D.bounds.center, boxCollider2D.bounds.size, 0, Vector2.down, 0.1f, groundLayer);
return hit.collider != null;
}
private bool OnWall()
{
RaycastHit2D raycastHit = Physics2D.BoxCast(boxCollider2D.bounds.center, boxCollider2D.bounds.size, 0, new Vector2(transform.localScale.x, 0), 0.1f, wallLayer);
return raycastHit.collider != null;
}
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 9d1c56aa3fe7782458345752cba31657
-29
View File
@@ -1,29 +0,0 @@
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);
}
}
}
-76
View File
@@ -1,76 +0,0 @@
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);
}
}
}
}
-97
View File
@@ -1,97 +0,0 @@
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
}
-29
View File
@@ -1,29 +0,0 @@
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);
}
}
}