Page 1 of 1

[ZScript] DYME – Dynamic Youth Music Engine

Posted: Wed Oct 09, 2019 3:00 pm
by Gustavo6046
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.

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);
        }
    }
}

Re: [ZScript] DYME – Dynamic Youth Music Engine

Posted: Wed Oct 09, 2019 3:27 pm
by Cherno
A great idea! It would be perfect for my The Chaos Engine TC.

Re: [ZScript] DYME – Dynamic Youth Music Engine

Posted: Wed Oct 09, 2019 4:55 pm
by Nash
Got any runnable examples? I understand what it's supposed to do, but a usage example would help people really see the bigger picture.

Re: [ZScript] DYME – Dynamic Youth Music Engine

Posted: Thu Oct 10, 2019 8:39 am
by MFG38
Now this sounds cool! Coincidentally, I was just wondering if adaptive music would be doable in GZDoom - and apparently it is. :P

Re: [ZScript] DYME – Dynamic Youth Music Engine

Posted: Thu Oct 10, 2019 10:16 am
by Slax
Dramatic Doom when?

Re: [ZScript] DYME – Dynamic Youth Music Engine

Posted: Fri Oct 18, 2019 2:01 pm
by Gustavo6046
This is a prototype, it has some syntax errors (I used extends by accident, reminiscent from UnrealScript and ZDCode 2. Whoops!)

I might make a runnable example soon, but I'd rather make Forester first and introduce DYME there instead.

Re: [ZScript] DYME – Dynamic Youth Music Engine

Posted: Sat Oct 19, 2019 9:12 pm
by XLightningStormL
Just make a runnable example with Manhunt music, be done with it lol