Merge pull request #76 from Pengu12345/alternate_input_system

Separation of input detection in it's own class + Addition of Coin Toss
This commit is contained in:
Jenny Crowe
2022-05-07 14:19:15 -07:00
committed by GitHub
48 changed files with 5314 additions and 24 deletions

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 913a0b390fee11c478e9b3d8fc3857a1
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,189 @@
using DG.Tweening;
using NaughtyBezierCurves;
using HeavenStudio.Util;
using System;
using System.Collections.Generic;
using UnityEngine;
namespace HeavenStudio.Games.Loaders
{
using static Minigames;
public static class NtrCoinLoader
{
public static Minigame AddGame(EventCaller eventCaller)
{
return new Minigame("coinToss", "Coin Toss \n [One coin at a time!]", "B4E6F6", false, false, new List<GameAction>()
{
new GameAction("toss", delegate { CoinToss.instance.TossCoin(eventCaller.currentEntity.beat, eventCaller.currentEntity.toggle); }, 7, false, parameters: new List<Param>()
{
new Param("toggle", false, "Audience Reaction", "Enable Audience Reaction"),
}),
new GameAction("set background color", delegate { var e = eventCaller.currentEntity; CoinToss.instance.ChangeBackgroundColor(e.colorA, 0f); }, 0.5f, false, new List<Param>()
{
new Param("colorA", CoinToss.defaultBgColor, "Background Color", "The background color to change to")
} ),
new GameAction("fade background color", delegate { var e = eventCaller.currentEntity; CoinToss.instance.FadeBackgroundColor(e.colorA, e.colorB, e.length); }, 1f, true, new List<Param>()
{
new Param("colorA", Color.white, "Start Color", "The starting color in the fade"),
new Param("colorB", CoinToss.defaultBgColor, "End Color", "The ending color in the fade")
} ),
new GameAction("set foreground color", delegate { var e = eventCaller.currentEntity; CoinToss.instance.ChangeBackgroundColor(e.colorA, 0f, true); }, 0.5f, false, new List<Param>()
{
new Param("colorA", CoinToss.defaultFgColor, "Background Color", "The background color to change to")
} ),
new GameAction("fade foreground color", delegate { var e = eventCaller.currentEntity; CoinToss.instance.FadeBackgroundColor(e.colorA, e.colorB, e.length, true); }, 1f, true, new List<Param>()
{
new Param("colorA", Color.white, "Start Color", "The starting color in the fade"),
new Param("colorB", CoinToss.defaultFgColor, "End Color", "The ending color in the fade")
} ),
});
}
}
}
namespace HeavenStudio.Games
{
//using Scripts_CoinToss;
public class CoinToss : Minigame
{
//Right now, you can only throw one coin at a time.
//..Which makes sense, you only have one coin in the original game
//Though it would need a bit of code rewrite to make it work with multiple coins
public static CoinToss instance { get; set; }
private static Color _defaultBgColor;
public static Color defaultBgColor
{
get
{
ColorUtility.TryParseHtmlString("#F7F742", out _defaultBgColor);
return _defaultBgColor;
}
}
private static Color _defaultFgColor;
public static Color defaultFgColor
{
get
{
ColorUtility.TryParseHtmlString("#FFFF83", out _defaultFgColor);
return _defaultFgColor;
}
}
[Header("Backgrounds")]
public SpriteRenderer fg;
public SpriteRenderer bg;
Tween bgColorTween;
Tween fgColorTween;
[Header("Animators")]
public Animator handAnimator;
public Boolean isThrowing;
public bool audienceReacting;
public PlayerActionEvent coin;
private void Awake()
{
instance = this;
isThrowing = false;
coin = null;
}
private void Update()
{
//nothing
}
private void LateUpdate()
{
//nothing
}
public void TossCoin(float beat, bool audienceReacting)
{
if (coin != null) return;
//Play sound and animations
Jukebox.PlayOneShotGame("coinToss/throw");
handAnimator.Play("Throw", 0, 0);
//Game state says the hand is throwing the coin
isThrowing = true;
this.audienceReacting = audienceReacting;
coin = ScheduleInput(beat, 6f, InputType.STANDARD_DOWN, CatchSuccess, CatchMiss, CatchEmpty);
//coin.perfectOnly = true;
}
public void CatchSuccess(PlayerActionEvent caller, float state)
{
Jukebox.PlayOneShotGame("coinToss/catch");
if(this.audienceReacting) Jukebox.PlayOneShotGame("coinToss/applause");
handAnimator.Play("Catch_success", 0, 0);
isThrowing = false;
}
public void CatchMiss(PlayerActionEvent caller)
{
Jukebox.PlayOneShotGame("coinToss/miss");
if(this.audienceReacting) Jukebox.PlayOneShotGame("coinToss/disappointed");
handAnimator.Play("Pickup", 0, 0);
isThrowing = false;
}
public void CatchEmpty(PlayerActionEvent caller)
{
handAnimator.Play("Catch_empty", 0, 0);
isThrowing = false;
coin.CanHit(false);
}
public void ChangeBackgroundColor(Color color, float beats, bool isFg = false)
{
var seconds = Conductor.instance.secPerBeat * beats;
if(!isFg)
{
if (bgColorTween != null)
bgColorTween.Kill(true);
} else
{
if (fgColorTween != null)
fgColorTween.Kill(true);
}
if (seconds == 0)
{
if(!isFg) bg.color = color;
if (isFg) fg.color = color;
}
else
{
if(!isFg) bgColorTween = bg.DOColor(color, seconds);
if(isFg) fgColorTween = fg.DOColor(color, seconds);
}
}
public void FadeBackgroundColor(Color start, Color end, float beats, bool isFg = false)
{
ChangeBackgroundColor(start, 0f, isFg);
ChangeBackgroundColor(end, beats, isFg);
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5e3903a882e6af341896ffb744aae583
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -6,7 +6,7 @@ namespace HeavenStudio.Games
{
public class Minigame : MonoBehaviour
{
public static float earlyTime = 0.84f, perfectTime = 0.91f, lateTime = 1.09f, endTime = 1.15f;
public static float earlyTime = 0.90f, perfectTime = 0.93f, lateTime = 1.06f, endTime = 1.10f;
public List<Minigame.Eligible> EligibleHits = new List<Minigame.Eligible>();
[System.Serializable]
@ -21,6 +21,123 @@ namespace HeavenStudio.Games
public float createBeat;
}
public List<PlayerActionEvent> scheduledInputs = new List<PlayerActionEvent>();
/**
* Schedule an Input for a later time in the minigame. Executes the methods put in parameters
*
* float startBeat : When the scheduling started (In beats)
* float timer : How many beats later should the input be expected
* InputType inputType : The type of the input that's expected (Press, release, A, B, Directions..) (Check InputType class for a list)
* ActionEventCallbackState OnHit : Method to run if the Input has been Hit
* ActionEventCallback OnMiss : Method to run if the Input has been Missed
* ActionEventCallback OnBlank : Method to run whenever there's an Input while this is Scheduled (Shouldn't be used too much)
*/
public PlayerActionEvent ScheduleInput(float startBeat,
float timer,
InputType inputType,
PlayerActionEvent.ActionEventCallbackState OnHit,
PlayerActionEvent.ActionEventCallback OnMiss,
PlayerActionEvent.ActionEventCallback OnBlank)
{
GameObject evtObj = new GameObject("ActionEvent" + (startBeat+timer));
evtObj.AddComponent<PlayerActionEvent>();
PlayerActionEvent evt = evtObj.GetComponent<PlayerActionEvent>();
evt.startBeat = startBeat;
evt.timer = timer;
evt.inputType = inputType;
evt.OnHit = OnHit;
evt.OnMiss = OnMiss;
evt.OnBlank = OnBlank;
evt.OnDestroy = RemoveScheduledInput;
evt.canHit = true;
evt.enabled = true;
evt.transform.parent = this.transform.parent;
evtObj.SetActive(true);
scheduledInputs.Add(evt);
return evt;
}
public PlayerActionEvent ScheduleAutoplayInput(float startBeat,
float timer,
InputType inputType,
PlayerActionEvent.ActionEventCallbackState OnHit,
PlayerActionEvent.ActionEventCallback OnMiss,
PlayerActionEvent.ActionEventCallback OnBlank)
{
PlayerActionEvent evt = ScheduleInput(startBeat, timer, inputType, OnHit, OnMiss, OnBlank);
evt.autoplayOnly = true;
return evt;
}
public PlayerActionEvent ScheduleUserInput(float startBeat,
float timer,
InputType inputType,
PlayerActionEvent.ActionEventCallbackState OnHit,
PlayerActionEvent.ActionEventCallback OnMiss,
PlayerActionEvent.ActionEventCallback OnBlank)
{
PlayerActionEvent evt = ScheduleInput(startBeat, timer, inputType, OnHit, OnMiss, OnBlank);
evt.noAutoplay = true;
return evt;
}
//Clean up method used whenever a PlayerActionEvent has finished
public void RemoveScheduledInput(PlayerActionEvent evt)
{
scheduledInputs.Remove(evt);
}
//Get the scheduled input that should happen the **Soonest**
//Can return null if there's no scheduled inputs
public PlayerActionEvent GetClosestScheduledInput()
{
PlayerActionEvent closest = null;
foreach(PlayerActionEvent toCompare in scheduledInputs)
{
if(closest == null)
{
closest = toCompare;
} else
{
float t1 = closest.startBeat + closest.timer;
float t2 = toCompare.startBeat + toCompare.timer;
Debug.Log("t1=" + t1 + " -- t2=" + t2);
if (t2 < t1) closest = toCompare;
}
}
return closest;
}
//Hasn't been tested yet. *Should* work.
//Can be used to detect if the user is expected to input something now or not
//Useful for strict call and responses games like Tambourine
public bool IsExpectingInputNow()
{
PlayerActionEvent input = GetClosestScheduledInput();
if (input == null) return false;
return input.IsExpectingInputNow();
}
// hopefully these will fix the lowbpm problem
public static float EarlyTime()
{

View File

@ -0,0 +1,170 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using DG.Tweening;
using HeavenStudio.Util;
using Starpelly;
namespace HeavenStudio.Games
{
public class PlayerActionEvent : PlayerActionObject
{
public delegate void ActionEventCallback(PlayerActionEvent caller);
public delegate void ActionEventCallbackState(PlayerActionEvent caller, float state);
public ActionEventCallbackState OnHit; //Function to trigger when an input has been done perfectly
public ActionEventCallback OnMiss; //Function to trigger when an input has been missed
public ActionEventCallback OnBlank; //Function to trigger when an input has been recorded while this is pending
public ActionEventCallback OnDestroy; //Function to trigger whenever this event gets destroyed. /!\ Shouldn't be used for a minigame! Use OnMiss instead /!\
public float startBeat;
public float timer;
public bool canHit = true; //Indicates if you can still hit the cue or not. If set to false, it'll guarantee a miss
public bool enabled = true; //Indicates if the PlayerActionEvent is enabled. If set to false, it'll not trigger any events and destroy itself AFTER it's not relevant anymore
public bool autoplayOnly = false; //Indicates if the input event only triggers when it's autoplay. If set to true, NO Miss or Blank events will be triggered when you're not autoplaying.
public bool noAutoplay = false; //Indicates if this PlayerActionEvent is recognized by the autoplay. /!\ Overrides autoPlayOnly /!\
public InputType inputType; //The type of input. Check the InputType class to see a list of all of them
public bool perfectOnly = false; //Indicates that the input only recognize perfect inputs.
public void setHitCallback(ActionEventCallbackState OnHit)
{
this.OnHit = OnHit;
}
public void setMissCallback(ActionEventCallback OnMiss)
{
this.OnMiss = OnMiss;
}
public void Enable() { enabled = true; }
public void Disable() { enabled = false; }
public void CanHit(bool canHit)
{
this.canHit = canHit;
}
public void Update()
{
if(!Conductor.instance.NotStopped()){CleanUp();} // If the song is stopped entirely in the editor, destroy itself as we don't want duplicates
if (noAutoplay && autoplayOnly) autoplayOnly = false;
if (noAutoplay && triggersAutoplay){ triggersAutoplay = false; }
float normalizedBeat = Conductor.instance.GetPositionFromBeat(startBeat,timer);
StateCheck(normalizedBeat);
if (normalizedBeat > Minigame.LateTime()) Miss();
if (IsCorrectInput() && !autoplayOnly)
{
if (state.perfect)
{
Hit(0f);
}
else if (state.early && !perfectOnly)
{
Hit(-1f);
}
else if (state.late && !perfectOnly)
{
Hit(1f);
}
else
{
Blank();
}
}
}
public bool IsExpectingInputNow()
{
float normalizedBeat = Conductor.instance.GetPositionFromBeat(startBeat, timer);
return normalizedBeat > Minigame.EarlyTime() && normalizedBeat < Minigame.EndTime();
}
public bool IsCorrectInput()
{
// This one is a mouthful but it's an evil good to detect the correct input
// Forgive me for those input type names
return (
//General inputs, both down and up
(PlayerInput.Pressed() && inputType == InputType.STANDARD_DOWN) ||
(PlayerInput.AltPressed() && inputType == InputType.STANDARD_ALT_DOWN) ||
(PlayerInput.GetAnyDirectionDown() && inputType == InputType.DIRECTION_DOWN) ||
(PlayerInput.PressedUp() && inputType == InputType.STANDARD_UP) ||
(PlayerInput.AltPressedUp() && inputType == InputType.STANDARD_ALT_UP) ||
(PlayerInput.GetAnyDirectionUp() && inputType == InputType.DIRECTION_UP) ||
//Specific directional inputs
(PlayerInput.GetSpecificDirectionDown(PlayerInput.DOWN) && inputType == InputType.DIRECTION_DOWN_DOWN) ||
(PlayerInput.GetSpecificDirectionDown(PlayerInput.UP) && inputType == InputType.DIRECTION_UP_DOWN) ||
(PlayerInput.GetSpecificDirectionDown(PlayerInput.LEFT) && inputType == InputType.DIRECTION_LEFT_DOWN) ||
(PlayerInput.GetSpecificDirectionDown(PlayerInput.RIGHT) && inputType == InputType.DIRECTION_RIGHT_DOWN) ||
(PlayerInput.GetSpecificDirectionUp(PlayerInput.DOWN) && inputType == InputType.DIRECTION_DOWN_UP) ||
(PlayerInput.GetSpecificDirectionUp(PlayerInput.UP) && inputType == InputType.DIRECTION_UP_UP) ||
(PlayerInput.GetSpecificDirectionUp(PlayerInput.LEFT) && inputType == InputType.DIRECTION_LEFT_UP) ||
(PlayerInput.GetSpecificDirectionUp(PlayerInput.RIGHT) && inputType == InputType.DIRECTION_RIGHT_UP)
);
}
//For the Autoplay
public override void OnAce()
{
Hit(0f);
}
//The state parameter is either -1 -> Early, 0 -> Perfect, 1 -> Late
public void Hit(float state)
{
if (OnHit != null && enabled)
{
if(canHit)
{
OnHit(this, state);
CleanUp();
} else
{
Blank();
}
}
}
public void Miss()
{
if (OnMiss != null && enabled && !autoplayOnly)
{
OnMiss(this);
}
CleanUp();
}
public void Blank()
{
if(OnBlank != null && enabled && !autoplayOnly)
{
OnBlank(this);
}
}
public void CleanUp()
{
OnDestroy(this);
Destroy(this.gameObject);
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f2119e14dfba60f4abe4d2410a5e1b1b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,34 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace HeavenStudio
{
public enum InputType : int {
//General
//-------
//Down
STANDARD_DOWN = 0,
STANDARD_ALT_DOWN = 1,
DIRECTION_DOWN = 2,
//Up
STANDARD_UP = 3,
STANDARD_ALT_UP = 4,
DIRECTION_UP = 5,
//Specific
//--------
//Down
DIRECTION_DOWN_DOWN = 6,
DIRECTION_UP_DOWN = 7,
DIRECTION_LEFT_DOWN = 8,
DIRECTION_RIGHT_DOWN = 9,
//Up
DIRECTION_DOWN_UP = 10,
DIRECTION_UP_UP = 11,
DIRECTION_LEFT_UP = 12,
DIRECTION_RIGHT_UP = 13
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 25e3511c55cd2f540abe014bf39e626b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -92,7 +92,7 @@ namespace HeavenStudio.Editor
}
else if(type is bool)
{
toggle.isOn = (bool)type;
toggle.isOn = System.Convert.ToBoolean(parameterManager.entity[propertyName]); // ' (bool)type ' always results in false
toggle.onValueChanged.AddListener(delegate
{

View File

@ -39,21 +39,24 @@ namespace HeavenStudio.Editor
private void Update()
{
if (gameOpen)
if(!Conductor.instance.NotStopped())
{
if (Input.GetKeyDown(KeyCode.DownArrow))
if (gameOpen)
{
UpdateIndex(currentEventIndex + 1);
if (Input.GetKeyDown(KeyCode.DownArrow))
{
UpdateIndex(currentEventIndex + 1);
}
else if (Input.GetKeyDown(KeyCode.UpArrow))
{
UpdateIndex(currentEventIndex - 1);
}
}
else if (Input.GetKeyDown(KeyCode.UpArrow))
{
UpdateIndex(currentEventIndex - 1);
}
}
if (Input.mouseScrollDelta.y != 0)
{
UpdateIndex(currentEventIndex - Mathf.RoundToInt(Input.mouseScrollDelta.y));
if (Input.mouseScrollDelta.y != 0)
{
UpdateIndex(currentEventIndex - Mathf.RoundToInt(Input.mouseScrollDelta.y));
}
}
}

View File

@ -7,6 +7,26 @@ namespace HeavenStudio
public class PlayerInput
{
//Clockwise
public const int UP = 0;
public const int RIGHT = 1;
public const int DOWN = 2;
public const int LEFT = 3;
// The autoplay isn't activated AND
// The song is actually playing AND
// The GameManager allows you to Input
public static bool playerHasControl()
{
return !GameManager.instance.autoplay && Conductor.instance.isPlaying && GameManager.instance.canInput;
}
/*--------------------*/
/* MAIN INPUT METHODS */
/*--------------------*/
// BUTTONS
public static bool Pressed(bool includeDPad = false)
{
bool keyDown = Input.GetKeyDown(KeyCode.Z) || (includeDPad && GetAnyDirectionDown());
@ -28,44 +48,80 @@ namespace HeavenStudio
public static bool AltPressed()
{
return Input.GetKeyDown(KeyCode.X) && !GameManager.instance.autoplay && Conductor.instance.isPlaying && GameManager.instance.canInput;
return Input.GetKeyDown(KeyCode.X) && playerHasControl();
}
public static bool AltPressedUp()
{
return Input.GetKeyUp(KeyCode.X) && !GameManager.instance.autoplay && Conductor.instance.isPlaying && GameManager.instance.canInput;
return Input.GetKeyUp(KeyCode.X) && playerHasControl();
}
public static bool AltPressing()
{
return Input.GetKey(KeyCode.X) && !GameManager.instance.autoplay && Conductor.instance.isPlaying && GameManager.instance.canInput;
return Input.GetKey(KeyCode.X) && playerHasControl();
}
//Directions
public static bool GetAnyDirectionDown()
{
return Input.GetKeyDown(KeyCode.UpArrow)
return (Input.GetKeyDown(KeyCode.UpArrow)
|| Input.GetKeyDown(KeyCode.DownArrow)
|| Input.GetKeyDown(KeyCode.LeftArrow)
|| Input.GetKeyDown(KeyCode.RightArrow);
|| Input.GetKeyDown(KeyCode.RightArrow)) && playerHasControl();
}
public static bool GetAnyDirectionUp()
{
return Input.GetKeyUp(KeyCode.UpArrow)
return (Input.GetKeyUp(KeyCode.UpArrow)
|| Input.GetKeyUp(KeyCode.DownArrow)
|| Input.GetKeyUp(KeyCode.LeftArrow)
|| Input.GetKeyUp(KeyCode.RightArrow);
|| Input.GetKeyUp(KeyCode.RightArrow)) && playerHasControl();
}
public static bool GetAnyDirection()
{
return Input.GetKey(KeyCode.UpArrow)
return (Input.GetKey(KeyCode.UpArrow)
|| Input.GetKey(KeyCode.DownArrow)
|| Input.GetKey(KeyCode.LeftArrow)
|| Input.GetKey(KeyCode.RightArrow);
|| Input.GetKey(KeyCode.RightArrow)) && playerHasControl();
}
public static bool GetSpecificDirectionDown(int direction)
{
KeyCode targetCode = getKeyCode(direction);
if (targetCode == KeyCode.None) return false;
return Input.GetKeyDown(targetCode) && playerHasControl();
}
public static bool GetSpecificDirectionUp(int direction)
{
KeyCode targetCode = getKeyCode(direction);
if (targetCode == KeyCode.None) return false;
return Input.GetKeyUp(targetCode) && playerHasControl();
}
private static KeyCode getKeyCode(int direction)
{
KeyCode targetKeyCode;
switch (direction)
{
case PlayerInput.UP: targetKeyCode = KeyCode.UpArrow; break;
case PlayerInput.DOWN: targetKeyCode = KeyCode.DownArrow; break;
case PlayerInput.LEFT: targetKeyCode = KeyCode.LeftArrow; break;
case PlayerInput.RIGHT: targetKeyCode = KeyCode.RightArrow; break;
default: targetKeyCode = KeyCode.None; break;
}
return targetKeyCode;
}
}
}