This is a compilation of vJass libraries intended to replace the old Cinematic System, which is over five years old already. This compilation itself has been in the making for two years now and represents a major leap forward in terms of speed, functionality and user-friendliness.

Like its predecessor, Cinema Workshop consists of three main components:

Transition - A library that facilitates gradual changes of various properties such as unit position.

Additional modules:

UnitTransition

UnitColorTransition

UnitScaleTransition

UnitFacingTransition

UnitPositionTransition

UnitWalk

TerrainFog

CineCam - A library that takes command of the game camera and gives the user more control over it.

CineScript - The core of the system, a powerful library for composing and playing sequences of events.

Additional modules:

UnitAnimationCineScript

UnitTransitionCineScript

EnvironmentCineScript

UtilityCineScript

EffectCineScript

SoundCineScript

CameraCineScript

SubtitleCineScript

The new design is completely modular, each component can be used independently from the other two. Transition and CineScript each come with a collection of extensions that can be included in your map as needed. When used for their primary purpose of making cinematics, the three otherwise independent components achieve a strong synergy. The demo map includes both a sample cinematic that uses the entire CinemaWorkshop as well as a sample code for a triggered spell that only uses CineScript.

Transition library:

libraryTransitionrequiresoptionalCineCam//*****************************************************************//* TRANSITION//*//* written by: Anitarf//*//* This library was designed to streamline the coding of gradual//* changes of values. It allows you to gradually change a value//* without having to deal with timers and lists of instances.//* Simply implement the Transition module or extend the//* Transition struct and you will automatically gain the//* following methods://*//* method startTransition( real duration, integer transitiontype, boolean autodestroy )//* method stopTransition()//*//* Your struct must declare an onTransition method that takes a//* real argument and returns nothing. This method will be called//* periodically for all instances that had startTransition called//* on them for the duration specified when startTransition was//* called. During this time, a factor value will gradually//* increase for the instance from 0.0 to 1.0 and will be passed//* to your onTransition method, where you can use it to gradually//* change whatever game value you are changing.//*//* If this sounds too complicated, look at some examples of use//* and you will see that it is actually quite simple. Plenty of//* such examples should already come bundled with this library so//* so I see no need to include one in this documentation.//*//* The transitiontype argument passed to the startTransition//* method allows you to tweak how the transition factor changes//* from 0.0 to 1.0. More details on this are available in the//* calibration section where the transition types are declared.//*//* The autodestroy argument passed to the startTransition method//* determines whether the struct will be automatically destroyed//* when the transition finishes or is stopped.//*//* If this library is used in the same map as CineCam, then it//* will synchronize with CineCam instead of using its own update//* period, ensuring that the transitions are in sync with camera//* movement.//*//* If you extend the Transition struct, the onTransition method//* will be evaluated instead of called, which is slightly slower.//* On the other hand, extending the Transition struct will not//* copy the entire transition code the way implementing the//* Transition module does, which saves a little over a kilobyte//* in map size, so you might prefer to not use the module in//* situations where speed isn't important.//*****************************************************************globalsconstantrealTRANSITION_UPDATE_PERIOD=0.02// How often should a transition be updated.constantintegerTRANSITION_TYPE_LINEAR=0// The transition factor will increase steadily for the duration of the transition.constantintegerTRANSITION_TYPE_SQUARE1=-1// The transition factor will increase slower at the start and faster at the end of the transition.constantintegerTRANSITION_TYPE_SQUARE2=-2// The transition factor will increase faster at the start and slower at the end of the transition.constantintegerTRANSITION_TYPE_SINE=-3// The transition factor will increase slower at the start and the end of the transition and faster in the middle.endglobalsfunctioninterfaceTransitionTypetakesreallinearFactorreturnsreal// You may also use functions following this interface to define a custom transition type.// END OF CALIBRATION SECTION// ================================================================moduleTransitionprivaterealtimeprivaterealdurationprivateintegertranstypeprivatebooleanautodestroyprivatebooleanrunning// Should the instance be kept on the list?privatebooleanactive// Is the instance still on the list?privatestaticthistypearrayVprivatestaticintegerN = 0privatestatictimerTprivatestaticmethodmaintakesnothingreturnsnothinglocalthistypethislocalintegeri=0localintegerj=0localrealfloopexitwheni>=thistype.Nsetthis=thistype.V[i]
if.runningthenstaticifLIBRARY_CineCamthenset.time=.time+CINECAM_UPDATE_PERIODelseset.time=.time+TRANSITION_UPDATE_PERIODendifif.time>=.durationthenset.time=.durationset.running=falseendifsetf=.time/.durationif.transtype==TRANSITION_TYPE_LINEARthen// Do nothing.elseif.transtype==TRANSITION_TYPE_SINEthensetf = (1.0 - Cos(f*bj_PI)) / 2elseif.transtype==TRANSITION_TYPE_SQUARE1thensetf = f*felseif.transtype==TRANSITION_TYPE_SQUARE2thensetf = 1.0 - fsetf = 1.0 - f*felsesetf = TransitionType(.transtype).evaluate(f)
endifcall.onTransition(f)
setthistype.V[j]=thissetj=j+1elseset.active=falseif.autodestroythencall.destroy()
endifendifseti=i+1endloopsetthistype.N=jstaticifnot(LIBRARY_CineCam) thenifthistype.N==0thencallPauseTimer(thistype.T)
endifendifendmethod// ----------------------------------------------------------------privatestaticmethodonInittakesnothingreturnsnothingstaticifLIBRARY_CineCamthencallOnCineCamUpdate(thistype.main)
elsesetthistype.T=CreateTimer()
endifendmethod// ----------------------------------------------------------------// public methods:publicmethodstartTransitiontakesrealduration, integertransitiontype, booleanautodestroyreturnsnothingstaticifLIBRARY_CineCamthenifduration<CINECAM_UPDATE_PERIODthensetduration=CINECAM_UPDATE_PERIODendifelseifduration<TRANSITION_UPDATE_PERIODthensetduration=TRANSITION_UPDATE_PERIODendifendifset.autodestroy=autodestroyset.transtype=transitiontypeset.duration=durationset.time=0.0ifnot.activethenstaticifnot(LIBRARY_CineCam) thenifthistype.N==0thencallTimerStart(thistype.T, TRANSITION_UPDATE_PERIOD, true, functionthistype.main)
endifendifsetthistype.V[thistype.N]=thissetthistype.N=thistype.N+1set.active=trueendifset.running=trueendmethodpublicmethodstopTransitiontakesnothingreturnsnothingset.running=falseendmethodendmodule// ================================================================structTransitionstubmethodonTransitiontakesrealfactorreturnsnothingendmethodimplementTransitionendstructendlibrary

libraryCineScriptrequiresLinkedList, TimerUtils//*****************************************************************//* CINESCRIPT//*//* written by: Anitarf//* requires: -LinkedList//* -TimerUtils//*//* This is a library intended primarily for cinematic composition//* and playback. It enables you to assemble scripts - each script//* contains a list of actions with matching timestamps which are//* then executed in order once the script is run.//*//* First, a quick API overview://*//* struct CineScript//* real x//* real y//* real angle//* real speed//* real time//*//* static method create() -> CineScript//* method destroy()//* method run()//* method stop()//* method globalX( real x, real y ) -> real//* method globalY( real x, real y ) -> real//* method globalAngle( real angle ) -> real//*//* struct ScriptAction//* readonly CineScript parent//* readonly real timestamp//* readonly real duration//*//* static method create( CineScript parent, real timestamp, real duration ) -> ScriptAction//* method destroy()//*//* The ScriptAction struct must be extended by new structs to//* actually do anything. These structs can declare their own .run//* method as defined by the Action interface, as well as .update//* and .stop methods if the action has a duration longer than 0.//* Many such extensions are provided in separate libraries.//*//* A ScriptAction is always created for a specific parent script.//* When that script is run, it will wait until it reaches the//* timestamp of the action, at which point it will call the .run//* method of that action. Furthermore, if the speed of the script//* is modified during the duration of an action, the .update//* method of that action will be called, allowing it to adapt to//* the new speed. This allows scripts to be fast-forwarded, run//* in slow motion or even be paused at any time. Likewise, if the//* script is stopped during the duration of an action, the .stop//* method of that action will be called.//*//* You can also skip to a specific timestamp by changing the time//* value, however actions with timestamps before that time value//* will not be run, so any changes they may make to the gamestate//* will not take effect.//*//* In addition to speed, a CineScript can have a custom position//* and facing direction. This way, you can for example script a//* combination of unit movements and special effects for a spell//* and then run this script at any point where the unit uses that//* spell. Unlike script speed, position and direction are meant//* to be set before the script is run. These values may still be//* modified once the script is running, but the position of//* already processed actions will not be updated. The methods//* .globalX, .globalY and .globalAngle can be used to convert an//* action's local coordinates to global values. The direction is//* measured in degrees.//*//* Destroying a script will also destroy all actions registered//* with it, so there is no need to keep track of them, however//* you may also destroy individual actions as long as the action//* being destroyed is not currently being processed.//*****************************************************************privateinterfaceAction// This method will never be called if the script is paused (speed set to 0.0),// so if the method contains division by speed that will never result in division by 0.methodruntakesnothingreturnsnothingdefaultsnothing// This method will never be called for instant actions (duration of 0.0),// so if the method contains division by duration that will never result in division by 0.methodupdatetakesrealtimeElapsedreturnsnothingdefaultsnothing// This method will never be called for instant actions (duration of 0.0),// so if the method contains division by duration that will never result in division by 0.methodstoptakesnothingreturnsnothingdefaultsnothingendinterface// END OF CALIBRATION SECTION // ================================================================privatekeywordaddAction// This CineScript method may not be called from outside this library.// It is instead called automatically when you create an action.structCineScript// Script properties.realx=0.0realy=0.0privatereala=0.0// Use the angle operators to read/modify.privatereals=1.0// Use the speed operators to read/modify.// Readonly properties.readonlyrealduration=0.0// Internal variables.privatetimertprivatebooleannew=true// Safety boolean in case a script is restarted while processing.privatebooleanrunning=false// Is the script currently running (does it have an active timer)?privatebooleanprocessing=false// Are script actions currently being executed?privaterealtimestampprivaterealtimeskip=0.0// From where is the script being played?privateListlist// The ordered list of actions.privateLinkpending// The next action to be processed.// ----------------------------------------------------------------// This method is called only from the create method of ScriptAction.// It can't be called from elsewhere beause it is private to this library.// It ensures all actions added to the list are properly ordered.methodaddActiontakesScriptActiona, realtimestamp, realdurationreturnsLinklocalLinkl=.list.lastif.duration < timestamp+durationthenset.duration = timestamp+durationendififl==0orScriptAction(l.data).timestamp<=timestampthenreturnLink.createLast(.list, integer(a))
endifloopexitwhenl.prev==0orScriptAction(l.prev.data).timestamp<=timestampsetl=l.prevendloopreturnl.insertBefore(integer(a))
endmethod// ----------------------------------------------------------------privatestaticmethodtimerEndCallbacktakesnothingreturnsnothingcallCineScript(GetTimerData(GetExpiredTimer())).stop()
endmethodprivatestaticmethodtimerCallbacktakesnothingreturnsnothinglocalCineScriptthis=CineScript(GetTimerData(GetExpiredTimer()))
localScriptActiona=ScriptAction(.pending.data)
set.new=falseset.processing=trueset.timestamp=a.timestamploopexitwhennot (.s>0.0) // If the script was paused then quit.calla.run() // Run the action.exitwhennot(.running) or.new// If an action stopped or restarted the script then quit.set.pending=.pending.nextexitwhen.pending==0// If this was the last action then quit.seta=ScriptAction(.pending.data)
exitwhena.timestamp>.timestamp// Next action needs a timer, quit.endloopset.processing=falseif.newornot (.s>0.0) ornot.runningthen// Script was destroyed and remade while processing,// it was paused or it was stopped, do nothing.elseif.pending!=0then// Script needs a wait before the next action, run the timer.callTimerStart(.t, (a.timestamp-.timestamp)/.s, false, functionCineScript.timerCallback)
else// The script finished running, run the finish timer.callTimerStart(.t, (.duration-.timestamp)/.s, false, functionCineScript.timerEndCallback)
endifendmethod// ----------------------------------------------------------------// Public methods:methodstoptakesnothingreturnsnothinglocalScriptActionalocalLinklif.runningthencallReleaseTimer(.t)
set.running=false// Loop through all the actions run so far.setl=.list.firstloopexitwhenl==.pendingseta=ScriptAction(l.data)
// Stop the action if it was run and is still running.ifa.timestamp>=.timeskipanda.timestamp+a.duration>.timestampanda.stop.existsthencalla.stop()
endifsetl=l.nextendloopset.timeskip=0.0endifendmethodmethodruntakesnothingreturnsnothingcall.stop()
set.timestamp=.timeskipset.pending=.list.firstloopif.pending==0thenreturnendifexitwhenScriptAction(.pending.data).timestamp>=.timestampset.pending=.pending.nextendloopset.t=NewTimer()
callSetTimerData(.t, this)
if.s!=0.0thencallTimerStart(.t, (ScriptAction(.pending.data).timestamp-.timestamp)/.s, false, functionCineScript.timerCallback)
endifset.running=trueset.new=trueendmethod// Potentially inline-friendly methods intended to be used mainly by script actions.methodglobalXtakesrealx, realyreturnsrealreturn.x + Cos(.a)*x - Sin(.a)*yendmethodmethodglobalYtakesrealx, realyreturnsrealreturn.y + Sin(.a)*x + Cos(.a)*yendmethodmethodglobalAngletakesrealanglereturnsrealreturn.a*bj_RADTODEG + angleendmethod// ----------------------------------------------------------------// Public operators:methodoperatortimetakesnothingreturnsrealif.runningthenif.s==0.0then// If the script is paused then return the stored timestamp.return.timestampelseif.processingthen// If the script is being processed then return current action's timestamp.returnScriptAction(.pending.data).timestampelseif.pending!=0then// If the script is waiting for the next action then calculate the timestamp.returnScriptAction(.pending.data).timestamp - TimerGetRemaining(.t)*.selse// If the script is finishing then calculate the timestamp.return.duration - TimerGetRemaining(.t)*.sendifendif// If the script is not running, return timeskip.return.timeskipendmethodmethodoperatortime= takesrealnewTimereturnsnothingif.runningthencall.stop()
set.timeskip=newTimecall.run()
elseset.timeskip=newTimeendifendmethodmethodoperatorspeedtakesnothingreturnsrealreturn.sendmethodmethodoperatorspeed= takesrealnewSpeedreturnsnothinglocalScriptActionalocalLinklifnewSpeed<0.0thensetnewSpeed=0.0// Correct faulty inputs.endif// Get the current timestamp of the script, then update the speed.set.timestamp=.timeset.s=newSpeed// If the script is running, update the timer and old actions.if.runningthencallPauseTimer(.t)
// Restart the timer if needed.ifnot(.processing) and.s>0.0thenif.pending!=0thencallTimerStart(.t, (ScriptAction(.pending.data).timestamp - .timestamp)/.s, false, functionCineScript.timerCallback)
elsecallTimerStart(.t, (.duration - .timestamp)/.s, false, functionCineScript.timerEndCallback)
endifendif// Loop through all the actions run so far.setl=.list.firstloopexitwhenl==.pendingseta=ScriptAction(l.data)
// Update the action if it was run and is still running.ifa.timestamp>=.timeskipanda.timestamp+a.duration>.timestampanda.update.existsthencalla.update(.timestamp-a.timestamp)
endifsetl=l.nextendloopendifendmethodmethodoperatorangletakesnothingreturnsrealreturn.a*bj_RADTODEGendmethodmethodoperatorangle= takesrealnewAnglereturnsnothingset.a = newAngle*bj_DEGTORADendmethod// ----------------------------------------------------------------// Constructor and destructor:staticmethodcreatetakesnothingreturnsCineScriptlocalCineScriptthis=CineScript.allocate()
set.list=List.create()
returnthisendmethodmethodonDestroytakesnothingreturnsnothing// Release the timer if needed.call.stop()
// Destroy all the actions belonging to the script.loopexitwhen.list.first==0callScriptAction(.list.first.data).destroy()
endloopcall.list.destroy()
endmethodendstruct// ================================================================structScriptActionextendsActionprivateLinkl// Readonly values.readonlyCineScriptparentreadonlyrealtimestampreadonlyrealdurationstaticmethodcreatetakesCineScripts, realtimestamp, realdurationreturnsScriptActionlocalScriptActionthis=ScriptAction.allocate()
ifduration<0.0thensetduration=0.0endifset.timestamp=timestampset.duration=durationset.parent=sset.l=s.addAction(this, timestamp, duration)
returnthisendmethodmethodonDestroytakesnothingreturnsnothingcall.l.destroy()
endmethodendstructendlibrary

Nice to see the this cinematic system finally converted into vJass. I don't see the previous "gc()" calls which, in itself, is a huge change for the better.

The local real array in the CineCam library could be turned into something more efficient, like a global array or a series of individual, properly named variables, due to the huge overhead of local arrays.

CineCam is the only library that I would call a mere conversion. Transition, while still functioning similarly to the old particle subsystem, is so much more general, functional and modular that it really is more of a replacement for, rather than an evolution of, its predecessor. CineScript goes even further, accomplishing things that weren't remotely possible with the old system - see for yourself, try pressing escape while the sample cinematic is playing. ;)

Don't get this the wrong way, I know you didn't mean to say that this was "just a port", I just wanted to point out anyway that considerable improvements have been made beyond the obvious "it's in vJass now", since the first post doesn't mention that beyond the initial description.

Quote:

The local real array in the CineCam library could be turned into something more efficient, like a global array or a series of individual, properly named variables, due to the huge overhead of local arrays.

Well, it has to be an array since I can't use individual variables in a loop. If there is such a big difference between local and global arrays, I could make it global. In the cases where I can replace it with locals, I wonder if declaring those local variables wouldn't be more of an overhead than using an already declared array, especially if I switch to a global one. Were any benchmarks ever done to indicate how many uses of an array justifies a local declaration?

Quote:

I hope that this sees some good public usage.

Is there anyone left making cinematics at all? Even I don't know if I'll be using this for anything beyond the sample cinematic I made for it. Well, I did use CineScript when making my action map and it was incredibly useful there, so I want to point out again that these libraries needn't always be used in tandem, there's plenty of situations where they can be applied individually.

Well, it has to be an array since I can't use individual variables in a loop. If there is such a big difference between local and global arrays, I could make it global.

As far as I know, when using a local array, 32 kilobytes of memory (4 bytes per array slot, 8192 slots) would be allocated each time the function starts, and deallocated when the function returns. In the case of a global array, 32 kilobytes of memory would be allocated upon map initialization, and never deallocated.

But local variables are quite faster than globals, so I think that the use of a local array is absolutely justified, at least in this case.

Perhaps you could use a linked list (similar to the one in grim001's ListModule) instead of an array when looping through the struct instances in the Transition library.

This should most definitely see some good usage, although WC3 modding is slowly dying.

Why would you be very sure of that? Consider that we aren't just dealing with a VM, we're dealing with a Blizzard VM.

We'd need to test this in a map where you can easily switch between having no and a few hundred declared variables/arrays (by the use of nested textmacros) and then do a hundred get/set operations (again with textmacros, no loops) and test how long that takes depending on the number of global variables in the map and depending on whether the get/set operations are being done on a global or a local, so four test cases in total. Someone with a working japi would need to do this since I don't trust fps tests with something as fast as variable operations, but this is really a subject for a new thread.

Quote:

Originally Posted by BBQ

As far as I know, when using a local array, 32 kilobytes of memory (4 bytes per array slot, 8192 slots) would be allocated each time the function starts, and deallocated when the function returns. In the case of a global array, 32 kilobytes of memory would be allocated upon map initialization, and never deallocated.

It was my understanding that arrays were allocated incrementally as needed, but maybe I misunderstood what this post says. In either case, if local variables are indeed faster than globals when there are many globals in the map then this is a moot point as the amount of operations I do on the local array should out-weight the cost of its allocation.

Quote:

Originally Posted by BBQ

Perhaps you could use a linked list (similar to the one in grim001's ListModule) instead of an array when looping through the struct instances in the Transition library.

I could do that, but the speed gain would be insignificant. If I was having performance issues with Transition, the first thing I would do would be to rewrite UnitPositionTransition, since that one is likely to see the most use and its onTransition method is uninlineable, so I would copy&paste the Transition code into it instead of implementing the module so that I could inline the onTransition method manually. If I were to go this far, then it would make sense to also use grim001's module, but considering how ugly this solution is I would rather not go this far unless I need to.

Why would you be very sure of that? Consider that we aren't just dealing with a VM, we're dealing with a Blizzard VM.

Okay, here's a post by grim001 that I stumbled upon:

"I once had the idea that it would be better to use only global vars in my engine in order to avoid repeatedly initializing locals. The result is that the whole engine ran about 20% slower.

According to Pipedream the speed of variable access depends on how many other vars there are in that scope. vJASS maps can create thousands of global vars for structs, so globals have a very long list to sort through, whereas locals typically only have a few.

So basically, whichever you have fewer of (locals or globals) should be faster, but you will always have more globals than locals unless you are a freak."

Perhaps you trust him more than you trust me.

Quote:

Originally Posted by Anitarf

We'd need to test this in a map where you can easily switch between having no and a few hundred declared variables/arrays (by the use of nested textmacros) and then do a hundred get/set operations (again with textmacros, no loops) and test how long that takes depending on the number of global variables in the map and depending on whether the get/set operations are being done on a global or a local, so four test cases in total. Someone with a working japi would need to do this since I don't trust fps tests with something as fast as variable operations, but this is really a subject for a new thread.

Unfortunately, textmacros cannot be nested.

Quote:

Originally Posted by Anitarf

It was my understanding that arrays were allocated incrementally as needed, but maybe I misunderstood what this post says. In either case, if local variables are indeed faster than globals when there are many globals in the map then this is a moot point as the amount of operations I do on the local array should out-weight the cost of its allocation.

While the thread you linked to is very awesome (thanks!), it doesn't seem to show whether the memory is allocated incrementally or not.

"An array in jass will always use 8192 elements. No matter if your only using 1 or 400. Its very bad to use arrays that you do not need to use. As each type in war3 uses a 4kb integer as its pointer. So you are using roughly 32kb of ram per array. Sure it doesn't seem like much but remember that types also have there own memory usage on the handling stack."

Wow, this is a really incredible system, wish I had noticed this earlier! Learning to use it now, I first thought of using your old system, but was put off by a lot of outdated stuff (which, still, worked really well in practice).

__________________

Quote:

[23-56-42] Captain Griffen orders Hakeem to say: /me is going to bed now, with Wulfy, and he hopes that he will come out again with his manhood still attached...