Linked Director Movies
DMX2004 has greatly improved the ability to use Linked Director Movies (LDMs), that is one Director movie playing inside of another Director movie. Because of this the explanations below will focus primarily on the use of LDMs in DMX2004 (For a primer on LDMs and their usage, with pre DMX2004 syntax, check out this article)
To create an LDM follow these steps:
- Select "Import...", set the "media" option to "Link to External Media", select the appropriate file and import
- You will now have a cast member of type "#movie" in your cast. Using the Property Inspector ensure that "Enable Scripts" has been selected under the "Linked Movie" tab
- Drag the member onto the stage. Play the movie and you will now have one Director Movie playing inside of another.
Idiosyncrasies of LDMs
Now all the fun begins. While you do much of your programming in LDMs as you would for a standard Director project there are enough idiosyncrasies that the process is not as trivial as the above 3 steps seem to imply. Below is a list of problem areas which can then explored and workarounds demonstrated.
- mouseEnter, mouseWithin, mouseLeave: improper scope
- During these events the handlers within an LDM execute their code within the scope of the stage or MIAW hosting the LDM and not within the LDM itself
- During these events the handlers within an LDM execute their code within the scope of the stage or MIAW hosting the LDM and not within the LDM itself
- prepareFrame not called when explicitly navigating to a frame
- When using syntax such as _movie.go(someFrame) within an LDM the prepareFrame event will not be called. Prepareframe only gets called when the playback head advances on its own from frame to frame. As a result looping code like _movie.go(_movie.frame) will result in prepareFrame never being called during the loop. In a 10 frame movie where the 10th frame has _movie.go(1) then frames 2 through 10 will have the prepareFrame call execute but the first frame will not
- When using syntax such as _movie.go(someFrame) within an LDM the prepareFrame event will not be called. Prepareframe only gets called when the playback head advances on its own from frame to frame. As a result looping code like _movie.go(_movie.frame) will result in prepareFrame never being called during the loop. In a 10 frame movie where the 10th frame has _movie.go(1) then frames 2 through 10 will have the prepareFrame call execute but the first frame will not
- stepFrame and timeout events not called
- Ldms do have unique actor list and timeout lists but these lists will not receive their respected calls, or it could be that the calls are never made inside of an LDM.
- Ldms do have unique actor list and timeout lists but these lists will not receive their respected calls, or it could be that the calls are never made inside of an LDM.
- LDMs don't really know themselves
- While communication within an LDM works very well (except for the mouse events mentioned above) the LDM has no way of extracting a reference to itself and passing that around. That is at the stage level you can say
tLdm = _movie.sprite(x).movie
This nicely holds a reference to the LDM movie object. From within an LDM however _movie will not work to create a true reference to an LDM. So an LDM has no way to tell other objects within a project who it really is (that is pass a reference around to itself).
- While communication within an LDM works very well (except for the mouse events mentioned above) the LDM has no way of extracting a reference to itself and passing that around. That is at the stage level you can say
- The number of channels in a movie can play havoc with communication
- If you stage has 10 channels and your LDM has 20 then you will not be able to communicate with channels 11 to 20 of the LDM. The stage needs to have at least as many channels as your largest LDM. Actually it turns out it is the movie originating a call sequence that needs to have at least as many channels as the movies (LDM or stage) that may be called during the sequence. So if a call orginates in an LDM of 10 channels and calls some handler in the stage that references sprite 11 of the stage, that sprite will not receive the call.
Finding Solutions
Let's deal with getting Ldms to know themselves first
Since you can get a direct reference to an ldm, in the movie holding the ldm, by using _movie.sprite(x).movie it makes sense that if you could pass this information into the LDM and store it someplace then the LDM could at any point consult this variable and be able to self reference. So that's exactly what we'll do.
The problem is where can we store this information. It needs to be someplace that is universally accessible within the LDM and unique to the ldm so if there are multiple instances on the stage all the instances won't be referencing the same variable. Well what turns out be a convenient location (thanks Guy McLoughlin!) is sprite(1).scriptInstanceList. If you never actually use sprite 1 during your project you can use it as a storage container for information that is locally scoped for you ldm. Since the scriptInstanceList is a linear list and we want to be able to store and retrieve multiple variables specific to the LDM's scope let's create a propList at index 1 of the scriptInstanceList and reference that for all our variables.
Drop this behaviour on your sprite holding the LDM.
property pSprite
on beginSprite me pSprite = sprite(me.spriteNum) pSprite.movie.sprite(1).scriptInstanceList[1]=[:] pSprite.movie.sprite(1).scriptInstanceList[1][#ldmMovie] = pSprite.movie pSprite.movie.sprite(1).scriptInstanceList[1][#parentMovie] = _movie.stage.movie pSprite.movie.sprite(1).scriptInstanceList[1][#parentSprite] = pSprite end
The interesting thing here is that the beginSprite event will fire before any of the LDMs internal events, such as prepareMovie, startMovie, fire. Also that fortunately sprite(1).scriptInstanceList is available before these internal events fire. So now within the LDM at any point in time you can get a reference to itself simply by calling:
sprite(1).scriptInstanceList[1][#ldmMovie]
you can even create an api to make this easier. In the LDM create a movie script with the following:
on getLdmProp aProp return sprite(1).scriptInstanceList[1][aProp] end
Now to extract a reference simply call
getLdmProp(#ldmMovie)
So why introduce those other variables, #parentSprite, #parentMovie? Well by having them there you have a quick and easy way to communicate with the movie and sprite holding the LDM.
Let's say you wanted to set up a system where you have LDMs fire a specific event in their parent movie, for example your LDM holds an animation and you want to know when the end is reached. On the last frame of the animation you could put the following code:
on exitFrame me getLdmProp(#parentMovie).sendSprite(getLdmProp(#parentSprite).spriteNum, #animationDone) end
Now on the sprite holding the LDM you can attach a behaviour like this
on animationDone me --do some stuff end
This opens up a lot of possibilities. Something that may come to mind here is why was the call structure from within the LDM set up as
parentMovieReference.sendSprite(parentSpriteNumber, #handler)
as opposed to
parentSpriteReference.hander()
since the parentSpriteReference is stored and can be quickly extracted with getLdmProp(#parentSprite). The problem with the second approach is that even though the reference is absolutely correct and pointing to the right sprite in the right scope (which is very cool, imho) a handler called this way will throw an error if it does not exist. Using sendSprite will have the code fail gracefully.
Deep nesting of LDMs
So with the ldm behaviour described above an LDM can successfully identify the stage as its parent movie. But what happens if one LDM (ldm_b) is nested inside of another (ldm_a) which is then nested inside of the stage. This line of code
pSprite.movie.sprite(1).scriptInstanceList[1][#parentMovie] = _movie.stage.movie
while true for ldm_a, is not true for ldm_b. Ldm_b's parent is actually ldm_a not the stage. So how can this code be modified to reflect that? Well since an LDM is now self aware you could try changing the code to something like
pSprite.movie.sprite(1).scriptInstanceList[1][#parentMovie] = getLdmProp(#ldmMovie)
This will work well as a behaviour applied to ldm_b, since ldm_b is inside ldm_a and ldm_a being an ldm has a #ldmMovie property. But if this behaviour is attached to ldm_a sitting on the stage it won't work since the stage does not have an #ldmMovie property. Does that mean that you need 2 behaviours, one to apply to LDMs at the stage level and another for LDMs nested inside of other LDMs. If you aren't already confused then this will make things even more confusing as you try to work on your movie. During authoring any given LDM will have to reference the stage as its parent, even though, ultimately it may be sitting inside another LDM.
To try and simplify this whole discussion here are a series of methods that can be placed in a movie script which will help with LDM communication.
--ldmApi (movie script)
on ldmDataContainer
if sprite(1).scriptInstanceList.count = 0 then sprite(1).scriptInstanceList[1] = [:] end if ldmDataContainer = sprite(1).scriptInstanceList[1] return ldmDataContainer
end
on setLdmProp aProp, aValue
(ldmDataContainer())[aProp] = aValue
end
on getLdmProp aProp
tValue = (ldmDataContainer())[aProp] return tValue
end
on ldmMovie
dc = ldmDataContainer() tMovie = dc.getaProp(#ldmMovie)
if voidP(tMovie) then --no ldmMovie found therefore we must be at the topmost level --so assign that movie to a variable tMovie = _player.activeWindow.movie dc[#ldmMovie] = tMovie end if
return tMovie
end
on ldmParentMovie
return getLdmProp(#parentMovie)
end
on ldmParentSprite
return getLdmProp(#parentSprite)
end
on ldmParentSpriteNum
return getLdmProp(#parentSpriteNum)
end
on ldmInit aMovie, aParentMovie, aParentBehavior
ldmData = ldmDataContainer() ldmData[#ldmMovie] = aMovie ldmData[#parentMovie] = aParentMovie ldmData[#parentBehavior] = aParentBehavior ldmData[#parentSpriteNum] = aParentBehavior.spriteNum ldmData[#parentSprite] = aParentMovie.sprite(aParentBehavior.spriteNum)
end
Now the behaviour to drop on LDMs becomes
--ldmBhvr (behaviour script)
on beginSprite me currentMovie = ldmMovie() ldmMovie = currentMovie.sprite(me.spriteNum).movie ldmMovie.ldmInit(ldmMovie, currentMovie, me) end
So as you are building an LDM based project create an external ldm castlibrary. Place the above 2 scripts into the cast and have every movie in the project link to this external ldm cast. This new behaviour will now work no matter how many levels deep you nest your LDMs and the behaviour will also work properly at the stage level. Just be sure to NOT use channel 1 to hold any sprites since this is where the unique ldm data is stored
The mouseEnter/Leave/Within scope problem
Well now that we have a method for an LDM to point to itself the issue of firing these troublesome mouseEvents in the right scope becomes relatively simple. Peter Sorenson's article pointed out a the idea of redirecting your calls during the incorrectly scoped mouse events. However since it was written pre DMX04 he was using cumbersome globals to achieve this. Now we can make use of the referencing system that has been set up. So a properly functioning mouse event behaviour can be set up like this:
property pLdmMovieRef
on beginSprite me pLdmMovieRef = ldmMovie() end
on mouseDown me -- mouseDown code goes here end
on mouseUp me -- mouseUp code goes here end
on mouseEnter me --redirect the event to the proper scope pLdmMovieRef.sendSprite(me.spriteNum, #mLdmMouseEnter) end
on mLdmMouseEnter me --mouseEnter code goes here end
on mouseLeave me --redirect the event to the proper scope pLdmMovieRef.sendSprite(me.spriteNum, #mLdmMouseLeave) end
on mLdmMouseLeave me --mouseEnter code goes here end