Linked Director Movies

From Director Online Wiki
Jump to: navigation, search

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:

  1. Select "Import...", set the "media" option to "Link to External Media", select the appropriate file and import
  2. 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
  3. 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.

  1. 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

  2. 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

  3. 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.

  4. 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).

  5. 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