[ZScript] DYME – Dynamic Youth Music Engine
Posted: Wed Oct 09, 2019 3:00 pm
This is a rather simple ZScript asset, that will aid you with your adaptive music endeavors. It supports horizontal mixing, and attenuation (assigning DYME to an anchor other than a player or global object). It also supports foreground effects, i.e. music effects that are played on top of a background music bar (in either the very beginning or very center), usually denoting specific situations or scenarios. It was inspired by the music mechanics in Untitled Goose Game and The Legend of Zelda: Breath of the Wild (link to video that shows some of such mechanics in Breath of the Wild).
It even has a Node.js-esque event loop!
Currently, it's supposed to be initialized by writing some ZScript, and defining sounds to represent sections of music (usually 2 or 4 bars) in SNDINFO. Soon, the former part will be replaced by a DYMEINFO lump parser. For now, stay tuned.
It even has a Node.js-esque event loop!
Currently, it's supposed to be initialized by writing some ZScript, and defining sounds to represent sections of music (usually 2 or 4 bars) in SNDINFO. Soon, the former part will be replaced by a DYMEINFO lump parser. For now, stay tuned.
Code: Select all
////////////////////////////////////////////////////////////////////////
// //
// '||'''|. '\\ //` '||\ /||` '||''''| //
// Dynamic || || \\// ||\\.//|| || . //
// Youth || || || || || ||'''| //
// Music || || || || || || //
// Engine .||...|' () .||. () .|| ||. () .||....| () //
// //
// (c) 2019 Gustavo Ramos Rehermann. Available under the MIT License. //
// //
////////////////////////////////////////////////////////////////////////
// A timeout. When it is done, it sends an event loop message
// to call the handler.
class Timeout extends Thinker {
double Duration;
DYMEEventLoop EventLoop;
DYMEEventHandler Handler;
void Tick() {
if (Duration >= 0) {
Duration -= 1/35;
if (Duration <= 0) {
EventLoop.Message(Handler);
Duration = -1;
}
}
}
static Timeout New(DYMEEventLoop EventLoop, DYMEEventHandler Handler, double Time) {
Timeout Timer = Timeout(New("Timeout"));
Timer.EventLoop = EventLoop;
Timer.Handler = Handler;
Timer.Duration = Time;
return Timer;
}
}
// A class to handle asynchronous events.
class DYMEEventLoop extends Thinker {
Array<Timeout> Timeouts;
Array<DYMEMessage> Queue;
void Tick() {
while (Queue.Size() > 0) {
Queue[0].Process();
Queue.Delete(0);
}
}
void Message(DYMEEventHandler Handler) {
let msg = DYMEMessage.New(Handler);
Queue.Push(msg);
msg.Registered();
}
uint SetTimeout(DYMEEventHandler Handler, double Time) {
Timeouts.Push(Timeout.New(self, Handler, Time));
return Timeouts.Size() - 1;
}
void ClearTimeout(uint Which) {
if (Which < Timeouts.Size())
Timeouts[Which].Duration = -1;
}
}
// A message in the event loop.
class DYMEMessage extends Thinker {
DYMEEventHandler Handler;
void Process() {
Handler.Handle();
}
void Registered() {
Handler.Registered();
}
static void New(DYMEEventHandler Handler) {
DYMEMessage msg = DYMEMessage(New("DYMEMessage"));
msg.Handler = Handler;
return msg;
}
}
// An event handler.
class DYMEEventHandler {
virtual void Registered () {};
virtual void Handle () {};
}
// A background section of a song.
class DYMEBackSection extends Thinker {
string Sound;
double Duration;
DYMEEventLoop EventLoop;
void Play(DYMEAnchor Anchor, DYMEEventHandler BackHandler = null, DYMEEventHandler FrontHandler = null, uint Attenuation = ATTN_None) {
Anchor.A_PlaySound(Sound, Attenuation);
if (FrontHandler) EventLoop.SetTimeout(FrontHandler, Duration / 2);
if (BackHandler) EventLoop.SetTimeout(BackHandler, Duration);
if (FrontHandler) EventLoop.SetTimeout(FrontHandler, Duration);
}
}
// A foreground section of a song.
class DYMEFrontSection extends Thinker {
string Sound;
double Duration;
void Play(DYMEAnchor Anchor, uint Attenuation = ATTN_None) {
Anchor.A_PlaySound(Sound, Attenuation);
}
}
// An actor to attach a DYME instance to.
class DYMEAnchor extends Actor {
Actor AttachedTo;
DYME Engine;
Default {
Radius 1;
Height 1;
}
States {
Spawn:
TNT1 A -1;
Stop;
}
static DYMEAnchor New(Vector3 Position) {
let Anch = Spawn("DYMEAnchor", Position);
return Anch;
}
static DYMEAnchor NewAttached(Actor To) {
let Anch = Spawn("DYMEAnchor", To.pos);
Anch.AttachedTo = To;
return Anch;
}
void Tick() {
SetXYZ(AttachedTo.pos);
}
}
// A base trigger class.
class DYMETrigger extends Actor {
Default {
+SPECIAL;
}
void Touch(Actor Other) {
DYMEAnchor anch;
if ((anch = DYMEAnchor(Other)) == null) return;
Trigger(anch.Engine);
}
virtual void Trigger(DYME Engine);
}
// A trigger class that pends to change the background track playing.
class DYMEBackgroundTrigger extends Actor {
DYMETrack NewTrack;
virtual void Trigger(DYME Engine) {
Engine.NextTrack = NewTrack;
}
}
// A trigger class that pends to play a new foreground section.
class DYMEForegroundTrigger extends Actor {
DYMEFrontSection FrontSection;
virtual void Trigger(DYME Engine) {
Engine.NextFrontSection = FrontSection;
}
}
// A special DYMEEventHandler subclass, common superclass of commonly used
// internal DYME handlers.
class DYMEEngineHandler extends DYMEEventHandler {
DYME Engine;
static New(DYME NEngine) {
DYMEForegroundHandler FH = DYMEForegroundHandler(New("DYMEForegroundHandler"));
Engine = NEngine;
}
}
// And its subclasses...
class DYMEForegroundHandler extends DYMEEngineHandler {
override void Handle() {
Engine.Foreground();
}
}
class DYMEBackgroundHandler extends DYMEEngineHandler {
override void Handle() {
Engine.NextSection();
}
}
// A single track.
class DYMETrack {
String Name;
Array<DYMEBackSection> Sections;
static void New(String Name) {
DYMETrack track = DYMETrack(New("DYMETrack"));
track.Name = Name;
return track;
}
void Add(DYMEBackSection Section) {
Sections.Push(Section);
}
DYMEBackSection Random() {
return Get(Random(0, Size() - 1));
}
DYMEBackSection Get(uint Index) {
return Sections[Index];
}
uint Size() {
return Sections.Size();
}
}
//== The main engine class. ==//
class DYME extends Thinker {
Array<DYMETrack> Tracks;
DYMETrack Playing;
DYMETrack NextTrack;
DYMEBackSection PlayingSection;
DYMEEventLoop EventLoop;
DYMEBackSection NextBackSection;
DYMEFrontSection NextFrontSection;
DYMEForegroundHandler ForegroundHandler;
DYMEBackgroundHandler BackgroundHandler;
// Called when it's time to switch to a new section (or track).
void NextSection() {
if (NextTrack != null) {
NextFrontSection = null;
NextSection = null;
SetPlaying(NextTrack);
NextTrack = null;
return;
}
if (NextSection == null)
SetPlayingSection(Playing.Random());
else
SetPlayingSection(NextSection);
NextSection = Playing.Random();
}
// Changes the playing track. Playing MUST be null.
void SetPlaying(DYMETrack Track) {
Playing = Track;
SetPlayingSection(Playing.Random());
}
// Changes the playing section.
void SetPlayingSection(DYMEBackSection Section) {
PlayingSection = Section;
PlayingSection.Play();
}
// Loads the handlers.
void PostBeginPlay() {
BackgroundHandler = BackgroundHandler(new("DYMEBackgroundHandler"));
ForegroundHandler = BackgroundHandler(new("DYMEForegroundHandler"));
}
// Adds a track.
void Add(Track T, bool AutoPlay = true) {
Tracks.Push(T);
// Autoplays only if no track is currently playing.
if (Playing == null && AutoPlay) {
SetPlaying(T);
}
}
}