• Listen to a special audio message from Bill Roper to the Hive Workshop community (Bill is a former Vice President of Blizzard Entertainment, Producer, Designer, Musician, Voice Actor) 🔗Click here to hear his message!
  • Read Evilhog's interview with Gregory Alper, the original composer of the music for WarCraft: Orcs & Humans 🔗Click here to read the full interview.

[Trigger] Dialogue system with unlimited transmissions

Status
Not open for further replies.
Level 6
Joined
Sep 20, 2012
Messages
179
Hello everyone.

Sorry if this problem has already been asked, but I could not find a thread.

So I am working on RPG/adventure campaign with tons of cinematic and dialogues. Cinematic really takes at least 80% of the whole gaming experience. The problem is it is really annoying to read so much text in a limited time and worry all the way that the transmission from a unit will disappear before you manage to get it through.

So what I want basically to do is to make such a system: every unit's transmission never ends until you skip it, so you can take so much time as you wish to read it. This system has some drawbacks, probably the worst being crash of smoothness of cinematic: for example, if a camera is applied, it will be applied and then it stops until you skip the current text, and, if it is applied smoothly after each transmission, the whole cinema will look pretty much broken. This is not so bad though if you do not overuse camera actions.

The problem is scripting such system. I am a bit familiar with JASS, but did not find there anything more useful here than in a usual trigger editor. The most efficient scheme I found right now is something like this.

  • Events
    • Unit - Grunt 0001 <gen> Dies
    • Conditions
    • Actions
      • Cinematic - Send transmission to (All players) from Grunt 0001 <gen> named Dead grunt: Play No sound and display I'm dead guys!. Modify duration: Set to 9999999.00 seconds and Don't wait
      • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
        • If - Conditions
          • TransmissionSkipped Equal to 1
        • Then - Actions
    • (????????????????????????????)
      • Else - Actions
        • Do nothing
      • Cinematic - Send transmission to (All players) from Grunt 0001 <gen> named Dead grunt: Play No sound and display I'm still dead. Modify duration: Set to 9999999.00 seconds and Don't wait
1. First of all, I have no idea what to write where I placed "???...???". I need to complete some actions in case a player skips the transmission too fast, before everything supposed to be done is done. No problem here. But then I need to go to the next transmission in the same trigger. Is there some function which immediately ends current transmission and starts another one?

2. Next. This "set to 9999999.00 seconds" looks cheesy. Is there a way to make a transmission really infinite, better than make it very long?

3. And one more. The TransmissionSkipped variable should be equal to 1 if a player skips the current transmission, it is not that hard to make. But how to apply the value to this variable during transmission? If I understand correctly, when a transmission starts, it is goes on to the end and only then the next action starts. So will I need to use a special separated trigger which checks every 0.01 seconds or so if the current transmission is stopped? Is there any better way?

A simple example with just 2 transmissions followed one by one and working system described above would be REALLY appreciated.

Thanks in advance guys!

P.S. Also one of the problems is that I have to write all these "if...then...else" after each transmission while I have thousands of them, which is pretty annoying. Is there a way to write some function that will do it automatically after each transmission?
 
Last edited:
Level 25
Joined
Jul 10, 2006
Messages
3,315
Setup:
  • Events
    • Map init
  • Actions
    • Set transmission[1] = "I'm dead guys!"
    • Set transmissionUnit[1] = Grunt 0001 <gen>
    • Set transmission[2] = "I'm still dead."
    • Set transmissionUnit[2] = Grunt 0001 <gen>
    • (set all the texts in squence like this)
Cinematic trigger:
  • Events
    • Cinematic starts
  • Actions
    • Cinematic - send transmission from transmissionUnit[1] named Dead Grunt no sound display transmission[1]
Skip to next:
  • Events
    • Player 1 skips a cinematic sequence
  • Actions
    • Set transmissionId = transmissionId + 1
    • Cinematic - display transmission[transmissionId] from transmissionUnit[transmissionId]
Furthermore, if you want to automatically end a specific transmission at an event,
  • Grunt Dies
  • Events
    • Grunt dies
  • Condition
    • Integer - transmissionId equal to 1
  • Actions
    • Set transmissionId = transmissionId + 1
    • Cinematic - display transmission[transmissionId] from transmissionUnit[transmissionId]
You can also add a maximum time to it; have a "cooldown" integer variable that decreases by 1 every second, when it reaches 0 you show the next transmission and reset it.

You should also attach a camera to each transmission phase, and loop setting that camera over 3 seconds every 0.25 seconds (the camera will never reach that camera, making it smoother)
 
Level 6
Joined
Sep 20, 2012
Messages
179
Thanks! That is exactly what I was looking for. Very nice idea to create arrays of texts. Just small additional thing:

  • Actions
  • Set transmissionId = transmissionId + 1
  • Cinematic - display transmission[transmissionId] from transmissionUnit[transmissionId]
There also should be a check if transmissionId has not reached its maximum limit.

I will try it on a test map and will place the exact triggers here after it works.
 
Last edited:
Level 6
Joined
Sep 20, 2012
Messages
179
OK, I have a problem now. I implemented something close to the offered above. That is what I am trying. My map has nothing but one grunt and one camera. Here are 3 triggers:

Main (initially on)
  • Main
    • Events
      • Time - Elapsed game time is 0.00 seconds
    • Conditions
    • Actions
      • Cinematic - Turn cinematic mode On for (All players)
      • Trigger - Run Cinematics <gen> (ignoring conditions)
      • Cinematic - Turn cinematic mode Off for (All players)
Cinematic (initially off)
  • Cinematic
    • Events
    • Conditions
    • Actions
      • Set i = 0
      • Set iMax = 2
      • Set tmCinema[1] = I'm dead guys!
      • Set tmCinema[2] = I'm still dead.
      • Set tmUnitName[1] = Dead grunt
      • Set tmUnitName[2] = Dead grunt
      • Set tmUnit[1] = Grunt 0000 <gen>
      • Set tmUnit[2] = Grunt 0000 <gen>
      • Camera - Apply Cam <gen> for Player 1 (Red) over 0.00 seconds
      • Trigger - Run Transmissions <gen> (ignoring conditions)
Transmissions (initially off)
  • Transmissions
    • Events
      • Player - Player 1 (Red) skips a cinematic sequence
    • Conditions
    • Actions
      • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
        • If - Conditions
          • i Less than (iMax+1)
        • Then - Actions
          • Set i = (i + 1)
          • Custom script: loop
          • Cinematic - Send transmission to (All players) from tmUnit[i] named tmUnitName[i]: Play No sound and display tmCinema[i]. Modify duration: Add 10.00 seconds and Don't wait
          • Custom script: endloop
        • Else - Actions
          • Skip remaining actions
So how it is supposed to work. Every time I want to use a sequence of transmission with no other actions between them, I set a pack of variables and just run a trigger Transmissions which will do all the work. It runs variable i from 1 to iMax (number of transmissions) and i is increasing only when a transmission is skipped. Once it reaches iMax+1, the trigger stops as does the last transmission. (EDIT: Actually last transmission stops not immediately but after some time. It can be fixed by changing the length of the transmission from 10 seconds to, say, 0.5 seconds. Surely not a very elegant solution.)

However it does nothing. When I run the map, everything goes fine until the camera is set up, then literally nothing happens. Cinematic mode is also turned off. It looks like the Transmission trigger does not run at all.

I wonder if it has something to do with trigger queue or something, though it is not clear why not a single transmission comes out then.
 
Level 25
Joined
Jul 10, 2006
Messages
3,315
The queueing problem is in the first trigger - the trigger won't wait for the cinematic trigger to be done before turning cinematic mode off.

You'll have to turn off cinematic mode in the transmission trigger after the last transmission has been sent.

What is the point of:
  • Custom script: loop
  • Custom script: endloop
The triggers I wrote down earlier was just theoretical, I'll see if I can make the trigger myself and post a working solution.
 
Level 6
Joined
Sep 20, 2012
Messages
179
I suppose I just know JASS not very well to understand how it works, so this loop...endloop structure may be useless.

I finally came up with an idea how to implement unlimited transmissions quick and easy with a bit of using JASS. We declare some boolean variable tmNext which is false by default. There is a trigger:

  • DialogueSystem
    • Events
      • Player - Player 1 (Red) skips a cinematic sequence
    • Conditions
    • Actions
      • Set tmNext = True
And a custom function:

JASS:
function tmNext takes nothing returns nothing
    loop
    call TransmissionFromUnitWithNameBJ( GetPlayersAll(), udg_tmUnit, udg_tmUnitName, null, udg_tmText, bj_TIMETYPE_SET, 0.01, true )
    exitwhen( udg_tmNext==true ) 
    endloop
    set udg_tmNext=false
endfunction

Every time we want to make a transmission, we set 3 variables for unit, unit name and text, then call this function (with no parameters), and that is it, very simple and convenient.

The problem is I cannot implement it into Trigger Editor. I tried to place the code of the function after a code of a different trigger but it failed. Where should it be? And am I right generally that, in contrary to running a trigger from a trigger, calling a function will really prevent other remaining actions before the loop cycle is finished?
 
BTW, you don't need to turn-off those two triggers as they don't have events anyway...

Also, I think that way of looping will give you an infinite loop which in turn will make wc3 crash... because there is no delay between the loops, it will loop and loop infinite times even before you can press the ESC key

I suggest putting the
  • Cinematic - Send transmission to (All players) from tmUnit[i] named tmUnitName[i]: Play No sound and display tmCinema[i]. Modify duration: Add 10.00 seconds and Don't wait
into a trigger run by a timer event, maybe 0.05 or 0.1 timer interval would be fine already...

tmNext -> integer variable here

  • Main
    • Events
      • Time - Elapsed game time is 0.00 seconds
    • Conditions
    • Actions
      • Set tmNext = 1
      • Set tmMax = 2
      • Set tmCinema[1] = I'm dead guys!
      • Set tmCinema[2] = I'm still dead.
      • Set tmUnitName[1] = Dead grunt
      • Set tmUnitName[2] = Dead grunt
      • Set tmUnit[1] = Grunt 0000 <gen>
      • Set tmUnit[2] = Grunt 0000 <gen>
      • Camera - Apply Cam <gen> for Player 1 (Red) over 0.00 seconds
      • Cinematic - Turn cinematic mode On for (All players)
      • Trigger - Turn-on Transmission <gen> (ignoring conditions)
  • Transmission
  • Events
    • Time - Every 0.1 seconds of game time
  • Actions
    • Cinematic - Send transmission to (All players) from tmUnit[tmNext] named tmUnitName[tmNext]: Play No sound and display tmCinema[tmNext]. Modify duration: Add 10.00 seconds and Don't wait
  • DialogueSystem
  • Events
    • Player - Player 1 (Red) skips a cinematic sequence
  • Conditions
  • Actions
    • Set tmNext = tmNext + 1
    • If-Then-else
      • If - Conditions
        • tmNext is greater than tmMax
      • Then - Actions
        • Trigger - Turn-off Transmission
        • Trigger - Turn-off (This Trigger)
Also, using this method, you can never make the transmission run a sound, else it would loop at the start over and over... Also, the portrait talk animation will be reset every interval so it might look bad too...
 
Level 6
Joined
Sep 20, 2012
Messages
179
Yes, this happened sometimes with looping while I was testing. As far as I am concerned, it is better to avoid looping at all when it is possible.

This idea with a timer sounds reasonable but there is a drawback. For every transmission I will have to make a special trigger, and while some dialogues include 30 transmissions or more, it will be really difficult to debug or rewrite the texts. It would be really good to find a way of somehow running the transmission by a single function call.

I thinks it can be done somehow even without JASS and with running one trigger from another, but then, again, I do not understand trigger queuing very well...

The problem would be immediately solved if there was some way to instantly stop a current transmission, regardless of its length. But even in JASS I fail to find a function any close to it. But I think the very transmission function contains some timer associated with it. If we could find the name of this timer's variable then it would be possible just to set it to 0 after event "cinematic is skipped".
 
You can just set all the transmissions using the arrays then... If the first cinematic starts at tmNext 1 and ends at tmMax 5, then set it as the first tmNext and tmMax then turn-on the transmissions trigger... then if the next one should run at tmNext 6 up to tmMax 10, then set them like that b4 you turn on the Transmissions trigger again during the time that you would need to run the cinematic...

Actually, looping might be your best friend in achieving this... also, looping is useful in a lot of things...

and no you cannot do it without JASS... why? because GUI is converted to JASS when a map is compiled, so in essence it's always JASS...
 
Level 6
Joined
Sep 20, 2012
Messages
179
Good point. So it is possible just to write one trigger for each cinematic which will fill one array with transmission texts (also units and unit names) and then use a different trigger for an actual transmission with a needed array index. Sounds good enough, though a bit manual.

Still it would be good to simplify this further by using a manually written JASS function. I would really like to know how to use it using only WE (I am aware there are special programs for working with JASS for Warcraft, but it should be possible to do this by custom function in Trigger Editor, only that I do not know how and where to actually declare it).
Then using transmissions will be as simple as that (without any arrays): set 3 variables and call a function, again set 3 variables and call the same function again and so on.

Sorry if I ask too many persistent questions, but I think it is essential to understand all this right now as otherwise I will be stuck on the same problems again and again.
 
just one trigger for all transmissions as long as the total number of trans is below 8191 (the max number of elements an array in wc3 can have)

You can place JASS scripts at the map header...

Setting 3 variables then calling a function PER transmission can be much harder to implement IMO...

if by functions you mean this:
JASS:
function tmNext takes nothing returns nothing
    loop
    call TransmissionFromUnitWithNameBJ( GetPlayersAll(), udg_tmUnit, udg_tmUnitName, null, udg_tmText, bj_TIMETYPE_SET, 0.01, true )
    exitwhen( udg_tmNext==true ) 
    endloop
    set udg_tmNext=false
endfunction

I assure you that it would crash wc3 due to infinite loop...
 
Level 6
Joined
Sep 20, 2012
Messages
179
Eh, actually, I got it to work this way with a small remaining problem. So I placed this code at the map header:

JASS:
function tmNext takes nothing returns nothing
    loop
    call TransmissionFromUnitWithNameBJ( GetPlayersAll(), udg_tmUnit, udg_tmUnitName, null, udg_tmText, bj_TIMETYPE_SET, 0.01, true )
    exitwhen( udg_tmNext==true ) 
    endloop
    set udg_tmNext=false
endfunction

Triggers:

  • Main
    • Events
      • Time - Elapsed game time is 0.00 seconds
    • Conditions
    • Actions
      • Cinematic - Turn cinematic mode On for (All players)
      • Camera - Apply Cam <gen> for Player 1 (Red) over 0.00 seconds
      • Set tmUnit = Grunt 0000 <gen>
      • Set tmUnitName = Dead grunt
      • Set tmText = I'm dead guys!
      • Custom script: call tmNext()
      • Set tmText = I'm still dead.
      • Custom script: call tmNext()
      • Cinematic - Turn cinematic mode Off for (All players)
  • DialogueSystem
    • Events
      • Player - Player 1 (Red) skips a cinematic sequence
    • Conditions
    • Actions
      • Set tmNext = True
It does not crash due to infinite loop, I suppose, as 100 times a second is not considered to be infinite by WC3. It actually runs next transmission after hitting ESC (not ideal, I would prefer hitting ENTER or mouse button, not ESC, but I have no idea how to make it work). However there is a problem you mentioned: the grunt's portrait is being reset constantly so it is quickly shaking which looks weird.

If this problem is resolved, this system may be implemented. Is there a way to stop the portrait resetting, other than trying to change the JASS transmission function itself?

Or there is another possible solution. If we could freeze or constantly reset the transmission timer, it would work just as well, even better since then there is no problem in adding sound to the transmission. But how to do this, what is the name of the timer's variable?
 
Level 6
Joined
Sep 20, 2012
Messages
179
YAHOO!!! I did it! After over 3 days!

After careful looking into JASS code of the transmission function

JASS:
function TransmissionFromUnitWithNameBJ takes force toForce, unit whichUnit, string unitName, sound soundHandle, string message, integer timeType, real timeVal, boolean wait returns nothing
    ...
    call DoTransmissionBasicsXYBJ(GetUnitTypeId(whichUnit), GetPlayerColor(GetOwningPlayer(whichUnit)), GetUnitX(whichUnit), GetUnitY(whichUnit), soundHandle, unitName, message, bj_lastTransmissionDuration)
    ...

I noticed that actually what creates a transmission is DoTransmissionBasicsXYBJ function, while other parts only set waits properly. So what if we work with this raw function? That is how it worked:

Head code:
JASS:
function Transmission takes nothing returns nothing
    call DoTransmissionBasicsXYBJ ( GetUnitTypeId(udg_tmUnit), GetPlayerColor(GetOwningPlayer(udg_tmUnit)), GetUnitX(udg_tmUnit), GetUnitY(udg_tmUnit), null, udg_tmUnitName, udg_tmText, 10000.00 )
    loop
        exitwhen (udg_tmSkipped==true)
        call TriggerSleepAction(0.01)
    endloop
    set udg_tmSkipped=false
endfunction

  • SkippedTm
    • Events
      • Player - Player 1 (Red) skips a cinematic sequence
    • Conditions
    • Actions
      • Set tmSkipped = True
  • Main
    • Events
      • Time - Elapsed game time is 0.00 seconds
    • Conditions
    • Actions
      • Cinematic - Turn cinematic mode On for (All players)
      • Camera - Apply Cam <gen> for Player 1 (Red) over 0.00 seconds
      • Set tmUnit = Grunt 0000 <gen>
      • Set tmUnitName = Dead grunt
      • Set tmText = I'm dead guys!
      • Custom script: call Transmission()
      • Set tmText = I'm still dead.
      • Custom script: call Transmission()
      • Cinematic - Turn cinematic mode Off for (All players)
I am not sure I understand how it really works, but I suppose it is something like this. Common transmission function from Trigger Editor has inside a special code for skipping transmissions so, whatever you use to skip them, it overrides your code and decides itself when to stop the transmission. Actual raw transmission without any modification is this DoTransmissionBasicsXYBJ function and it can be easily stopped by a custom code.

Interesting thing is that there is no portrait reset between two transmissions if they are done by the same unit. I have no idea why it happens but, I think, it is even smoother and better for cinematic.


Problem solved.
 
its just almost the same as one of your suggestions, setting to a high value... btw, you can further improve that code... see how its highlighted in red? that means its a BJ function, which means it calls another function... you can narrow it down to whatever function it calls...

btw, waits are actually not that accurate and have a high variability in its output...
 
Level 6
Joined
Sep 20, 2012
Messages
179
Yes, it does not look perfect with 10000 seconds. But in principle it is closer to what I would call the perfect solution - setting the timer to infinity. It is better than constantly resetting the timer which is just unnatural.

I could, but my knowing of JASS is rather small right now. I will experiment with narrowing down later though.

Hmm... Is there a better way than waits in this looping? I tried to change the wait function to the DoNothing() one, but it crashes WC3 since it seems to it as an infinite looping. I am aware they say generally in programming that you should avoid such functions as sleep() or delay() in C++, for example, but sometimes you really need them.

By the way, I wonder, does the wait function temporarily stops all the triggers or does it stop only the current trigger, letting other triggers work? I.e., for example, if I have also an active trigger which every 1 second orders a hero to make an attacking movement, will he do it during transmission waiting? If not, the solution described above does not work properly in some cases.
 
Level 6
Joined
Sep 20, 2012
Messages
179
Yes, I think so too. If wait stopped the whole game just because of some one trigger, then it would be literally useless and often would ruin games.

However there is another problem I have now. It is slightly off-topic, but I do not want to create a separated thread for this. Strangely, I could not find any solution for this very common problem: many people asked it on forums but no one got an appropriate answer.

The problem is this. I have a few music songs that I want to play in a shuffle mode, so that every time a music stops, a different one starts randomly (except for it should differ from the previous). In other words, I want to create a music list which will shuffle songs. Also I want it to play only aside from cinematic, while in cinematic mode there will be a special song for each cinematic - so let's call it "stream music". Stream music should change after some quests or events: for example, the map starts deep in the forest where 3 Chinese traditional songs play, but after some quests action moves to plains where stream music changes into 4 Middle-Eastern compositions.
However I am completely stuck on these music lists. A function "SetMapMusic" which should create music lists works very strangely. So right now I have found only this solution: I create a trigger which will start after any cinematic ends. It chooses randomly a song from an array of songs and waits until a song is played, then it starts again and chooses another song and so on. The trigger is turned off during cinematic mode. There are many complications with this, for example, sometimes I need just to pop one song from the list and change it into another one, but for this I will have to create an entire new array with all 3 songs, and there will be many such parts of the trigger if songs change often (and yes, they do in my map).
The problem is there is no music type variables by default, but the Trigger Editor basic functions have an argument of that type. So it is really unmanagable.

Is there any better solution than this? I have so many general triggers already and do not want to add them constantly, or one day they may start interfere with each other. Can I do all this by simply setting a music list every time cinematic ends? If so, how can it be done (exactly)?
 
Level 6
Joined
Sep 20, 2012
Messages
179
OK, I will explain what I have done by now about this music problem and how it fails. So I wrote this function:

JASS:
function playStreamMusic takes nothing returns nothing
    loop
        set udg_randomMusic = GetRandomInt(1, udg_MusicListSize)
        if ( not( (bj_lastPlayedMusic==udg_MusicList[udg_randomMusic]) and (udg_MusicListSize>1) ) and (udg_CinematicsOn==false) ) then
            call PlayMusic(udg_MusicList[udg_randomMusic])
            call TriggerSleepAction( GetSoundFileDuration(udg_MusicList[udg_randomMusic]) )
        endif
        call TriggerSleepAction(0.1)
    endloop
endfunction

MusicList is an array containing string variables with names of songs I am using, MusicListSize - number of songs in the current playlist. There is a short trigger:

  • StreamMusic
    • Events
    • Conditions
      • MusicListSize Greater than 0
    • Actions
      • Custom script: call playStreamMusic()
(I need a separate trigger for this since otherwise sleep actions will freeze the game.)

This trigger runs every time cinematic ends (with killing this trigger queue to avoid leaks here).

But it does not work! I tested it on a 1-song playlist. After cinematic ends music actually starts, but when it comes to end - it does not repeat again!
If I change PlayMusic and GetSoundFileDuration into the BJ ones, then music repeats, but strangely it is cut somewhere on the way to the end (90% I think), then it starts again, before finishing.

From the precious experience in this thread I learned that the more close to native functions you use, the better (normally). But here I am completely stuck: how can BJ and native functions be so different that in one case music repeats and in another - does not?

P.S. I found a few threads where people said that the wait action is not exact and for long periods of time (as in my case, when the length of the song is above 40 seconds) gives some percentage of error. That may be the reason songs play only 90% of their length and then start from the beginning. Time to implement a timer I think...
 
Last edited:
Level 6
Joined
Sep 20, 2012
Messages
179
Alright, never mind, the problem is solved. I will post the solution here as some people may also look for it and, looks like, there is no solution yet out in the Internet.

JASS:
function playStreamMusic takes nothing returns nothing
    call StartTimerBJ( udg_streamMusicTimer, false, 0.00 )
    loop
        exitwhen (udg_CinematicsOn==true)
        if ( (udg_MusicListSize>0) and (udg_CinematicsOn==false) and (TimerGetRemaining(udg_streamMusicTimer)<=0) ) then
            set udg_randomMusic = GetRandomInt(1, udg_MusicListSize)
            if ( ( (not(bj_lastPlayedMusic==udg_MusicList[udg_randomMusic])) and (udg_MusicListSize>1) ) or (udg_MusicListSize==1) ) then
                call PlayMusicBJ(udg_MusicList[udg_randomMusic])
                call StartTimerBJ( udg_streamMusicTimer, false, GetSoundFileDurationBJ(udg_MusicList[udg_randomMusic]) )
            endif
        endif
    call TriggerSleepAction(0.01)
    endloop
endfunction

  • StreamMusic
    • Events
    • Conditions
      • MusicListSize Greater than 0
    • Actions
      • Custom script: call playStreamMusic()
Whenever this trigger is run, it plays music in shuffle mode from the playlist MusicList[] which contains names of the songs (with war3mapImported\).


THREAD CAN BE CLOSED.
 
Level 6
Joined
Sep 20, 2012
Messages
179
I just did not want to create a single huge message. But OK, if it is preferred here, I will do so next time.
 
Status
Not open for further replies.
Top