[ZScript] Half-Life Style VOX Sentences

Tue Sep 14, 2021 10:07 am

I was wondering if there is a way to play sequences of sounds by writing them out in a string similarly to how Half-Life does.

For example here is a announcement sequence defined in Half-Life:
Code:
1A3_5 (p130) bizwarn bizwarn bizwarn (e95 p95) attention.  this announcement system now(e100) under military command


Right now I'm using a two dimensional array to hold my sequences but this is not ideal, especially for readability reasons.

An example:
Code:
Class AlphaHEVSystem : Thinker
{
   Actor Player;
   Actor Plr;
   Sound FVOX[201][25];
   Int SeqNum;
   Int SetupTics;
   Bool SeqComplete;
   Int Power;
   Int Priority;
   Int NewPriority;
   Bool Playing;
   Bool NewSeq;
   Int SeqNumber;
   Int NewSeqNumber;
   Int CVarLevel;
   Bool RArmorFail;
   Bool LowPower;
   Bool NoPower;

   Override Void PostBeginPlay(Void)
   {
      Super.PostBeginPlay();

      Let Plr = AlphaPlayerBaseClass(Player);

      SeqNum = 0;
      SetupTics = 0;
      SeqComplete = False;
      Power = Plr.CountInv("AlphaPowerCell",AAPTR_DEFAULT);
      Priority = 0;
      NewSeq = False;
      SeqNumber = 999;
      NewSeqNumber = 999;
      RArmorFail = True;
      LowPower = True;
      NoPower = True;

      FVOX[0][0] = "HEV/Comma";
      FVOX[0][1] = "HEV/Comma";
      FVOX[0][2] = "HEV/Logon0";
      FVOX[0][3] = "HEV/Comma";
      FVOX[0][4] = "HEV/Logon1";
      FVOX[0][5] = "HEV/Comma";
      FVOX[0][6] = "HEV/Logon2";
      FVOX[0][7] = "HEV/Comma";
      FVOX[0][8] = "HEV/Logon3";
      FVOX[0][9] = "HEV/Comma";
      FVOX[0][10] = "HEV/Logon4";
      FVOX[0][11] = "HEV/Comma";
      FVOX[0][12] = "HEV/Logon5";
      FVOX[0][13] = "HEV/Comma";
      FVOX[0][14] = "HEV/Logon6";
      FVOX[0][15] = "HEV/Comma";
      FVOX[0][16] = "HEV/Logon7";
      FVOX[0][17] = "HEV/Comma";
      FVOX[0][18] = "HEV/Logon8";
      FVOX[0][19] = "HEV/Online";
      FVOX[0][20] = "HEV/Comma";
      FVOX[0][21] = "HEV/Logon9";
      FVOX[0][22] = "HEV/Comma";
      FVOX[0][23] = "HEV/LogonComplete";
      FVOX[0][24] = "HEV/Period";

      FVOX[1][0] = "HEV/Beep";
      FVOX[1][1] = "HEV/Beep";
      FVOX[1][2] = "HEV/Comma";
      FVOX[1][3] = "HEV/Beep";
      FVOX[1][4] = "HEV/Beep";
      FVOX[1][5] = "HEV/Comma";
      FVOX[1][6] = "HEV/Beep";
      FVOX[1][7] = "HEV/Comma";
      FVOX[1][8] = "HEV/Beep";
      FVOX[1][9] = "HEV/Comma";
      FVOX[1][10] = "HEV/Flatline";
      FVOX[1][11] = "HEV/Period";
}


The function that actually plays sequences.
Code:
Bool PlayHEVSequence(Int Sequence = 0, Int Channel = 20, Int SeqPriority = 1)
   {
      Let Plr = AlphaPlayerBaseClass(Player);

      If (Plr)
      {
         If (((SeqPriority <= Priority || SeqPriority == 1) && SeqPriority != 0) || Priority == 0)
         {
            Priority = SeqPriority;

            If (SeqNum == 0)
            {
               SeqNumber = Sequence;
            }

            If (!Plr.IsActorPlayingSound(Channel))
            {
               If (SeqNum == 24)
               {
                  SeqNum = 0;
                  Return True;
               }

               Plr.A_StartSound(FVOX[SeqNumber][SeqNum],Channel,CHANF_UI,1.0,999.9,1.0,0.0);
               SeqNum++;
            }
         }
         Else
         {
            NewSeq = False;
            NewSeqNumber = 0;
            NewPriority = 0;
         }
      }

      Return False;
   }


The method I'm currently using does work but it is a pain to add sequences and quickly bloats the line count and can be frustrating to find a specific sequence when fixes or adjustments need to me made.

Re: [ZScript] Half-Life Style VOX Sentences

Wed Sep 15, 2021 1:20 am

You might try using a dictionary to store word to sound mappings. If you want to pass the entire sentence, split the string, e.g.

Code:
void PlayHEVSequence(string phrase)
{
    Array<string> tokens;
    phrase.Split(tokens, " ", TOK_SKIPEMPTY);
   
    // TODO: Do something with tokens
}

Find the mapping for each token to its corresponding sound using the dictionary. It's up to you what to do when no mapping exists (ignore it or throw an exception). What I'd do next is add the sounds to an array that is handled by the thinker's Tick method. I'd use the array like a queue by storing the current index in the array as a pointer to the sound that should be played next. Each tick, check if we're not playing a sound right now (you should probably also ensure that there is a delay between them), and in this case, see if the current index is valid (e.g. less than the current size of the array). If that's also true, then start playing a new sound and advance the index by 1. You will also have to clear the array periodically (and reset the index accordingly) so as not to run out of memory.

If you want to, I could try to provide more example code later today. I'm a little busy at work right now, sorry about that.

Re: [ZScript] Half-Life Style VOX Sentences

Wed Sep 15, 2021 7:26 pm

Wow that looks pretty complicated, not that the way I'm doing it now isn't, I just thought that for some reason there must be a simpler method. I would like to see an example if you have the time, however I must be honest, after writing over 3000 lines, I'm pretty invested now in my own method. Still it would be nice to see some alternatives for future use as my method is a real pain to work with.

EDIT:
Quite honestly all of this could be avoided if SNDSEQ was expanded and given more features like channel control, pitch, and all of the stuff regular sounds have. It would be much easier to define sequences in SNDSEQ but I absolutely need channel control and none of the functions for playing sequences through actors have that. If A_StartSound was given the option to play a sound sequence defined in SNDSEQ, that would be enough.

Re: [ZScript] Half-Life Style VOX Sentences

Thu Sep 16, 2021 12:26 am

OK, so here is a simplified but working example. It maps words to sounds in the form of: %s -> vox/%s, where %s is the word, so it doesn't actually need a dictionary. The code, however, can be extended to use one, or you can even implement your own rules to map words to sounds.

Code:
class Announcer : Actor
{
    Default
    {
        +NOINTERACTION;
    }

    private Array<string> _soundQueue;
    private int _queueIndex;
    private int _nextPlayTime;

    const SOUND_DELAY = 15; // adjust this to your liking

    static void Announce(string phrase)
    {
        let it = ThinkerIterator.Create('Announcer');
        let announcer = Announcer(it.Next());

        if (announcer == null)
        {
            announcer = Announcer(Spawn('Announcer'));
        }

        announcer.DoAnnounce(phrase);
    }

    private void DoAnnounce(string phrase)
    {
        Array<string> tokens;
        phrase.Split(tokens, " ", TOK_SKIPEMPTY);

        for (int i = 0; i < tokens.Size(); i++)
        {
            PushWordToQueue(tokens[i]);
        }
    }

    private void PushWordToQueue(string word)
    {
        // TODO: Map words to sounds here via Dictionary, if necessary,
        // use custom rules, handle non-existing sounds etc.
        _soundQueue.Push(string.Format("vox/%s", word));
    }

    override void Tick()
    {
        Super.Tick();

        // Check if we're currently playing something
        if (IsPlayingSound())
        {
            // Update the time when we will play the next sound
            _nextPlayTime = Level.maptime + SOUND_DELAY;

            return;
        }

        // Check if we still have sounds in queue and it is due to play another one
        if (_queueIndex < _soundQueue.Size() && Level.maptime >= _nextPlayTime)
        {
            // It is time to play the next sound now
            DoPlaySound(_soundQueue[_queueIndex]);

            // Advance the queue
            _queueIndex++;
        }

        // Check if we've reached the end of the queue, clear it if that's the case
        if (_queueIndex == _soundQueue.Size())
        {
            _soundQueue.Clear();
            _queueIndex = 0;
        }
    }

    private void DoPlaySound(string snd)
    {
        Console.Printf("DEBUG: Playing sound: %s", snd);

        // Play the sound at full volume regardless of where the player is
        A_StartSound(snd, CHAN_VOICE, attenuation: ATTN_NONE);
    }

    private bool IsPlayingSound()
    {
        return IsActorPlayingSound(CHAN_VOICE);
    }
}

I'm also attaching a WAD file with some sounds imported from Half-Life to demonstrate how it works. Type give testinv in the console to trigger an announcement.
You do not have the required permissions to view the files attached to this post.