-- CAMERA FOLLOWING -- -- -- © May-July 2002, James Newton -- -- This time-based script deals with four aspects of 3D movement: -- * arrow-key driven 2D navigation of a piloted model -- * moving a camera in an elastic manner behind the piloted model -- * avoiding collisions between the piloted model and other objects -- * ensuring that the camera never passes behind an object where its -- view of the piloted model is obscured -- -- Main improvements: -- * This script is fully self-contained. There is no need to create -- extra models or cameras. -- * 3D movement is independent of frame rate -- * The script can be used both as a behavior and as a parent script -- -- Possible future extensions: -- + terrain following -- + 3D navigation (aerobatics) -- -- PARAMETERS -- -- The initial parameters are: -- * sprite containing a 3D member -- * model to follow -- -- Other parameters such as the minimum distance from the camera to -- the model to follow, maximum and minimum speed, acceleration, -- friction and so on, can be set using a property list. Default -- values will be used if none are provided. -- -- This script assumes that all objects with which the pilot model -- might collide are as big or bigger and located at the same height -- as the center of the pilot model. If this is not the case, be -- sure to surround the other object with an invisible box model of -- suitable dimensions. -- -- PERFORMANCE -- -- This behavior uses the modelsUnderRay() method at least twice per -- frame. The performance of modelsUnderRay() decreases as the number -- of models and the total number of polygons increases. Few high- -- polygon models has less effect than many low-polygon models. To -- ensure that the frame rate is acceptable even on low end machines, -- a total count of less than 600 low-polygon models (< 10 polygons -- per model) or less than 16 high-polygon models (> 6500 polygons per -- model) is recommended. -- PROPERTY DECLARATIONS -- property spriteNum -- author-defined parameters property pilotModel -- model which the camera is following property speed -- current speed in units per millisecond property maxSpeed -- maximum speed in units per millisecond property minSpeed -- if speed falls below minSpeed, movement stops property power -- gain in speed per millisecond (up arrow) property brake -- loss of speed per millisecond (down arrow) property turnAngle -- degrees to turn per millisecond (left-right) property friction -- proportion of speed lost per millisecond property impactTime -- time taken to reach an obstacle property closeUp -- closest distance of camera to pilot model property rigidity -- the greater the value, the closer the camera -- adheres to the pilot model -- private properties property pSprite -- sprite containing the shockwave3D member property pWorld -- shockwave3D member property pCamera -- the camera used to view the world property pCameraLoc -- pCamera.worldPosition property pVector -- line of sight for camera property pFraction -- fraction of pVector for next camera movement property pLastSight -- milliseconds when camera lost sight of model property pLastUpdate -- the milliseconds on the last update property pElapsedTime -- number of milliseconds since last update -- driving property pThrust -- property pTurn -- -1 | O | +1 if left/right arrow keys pressed -- boundary detection property pRadius -- radius of cylinder surrounding pilot model property pLeftEdge -- transform used for boundary detection property pRightEdge -- transform used for boundary detection property pLeftRay -- source position of ray to left of cylinder property pRightRay -- source position of ray to right of cylinder -- SPRITE EVENT -- on beginSprite(me) --------------------------------------------------- -- RECEIVED when this script is used as a behavior, in which case -- the parameters will have been set -- ACTION: Converts the parameters to units per millisecond, rather -- than units per second -------------------------------------------------------------------- pSprite = sprite(spriteNum) pWorld = pSprite.member pCamera = pSprite.camera tModel = pWorld.model(pilotModel) if ilk(tModel, #model) then pilotModel = tModel -- Set the transform to "parent" the rest position of the camera tInverse = pilotModel.getWorldTransform().inverse() pCameraLoc = pCamera.worldPosition else me.mTreatError() end if speed = speed / 1000.0 minSpeed = minSpeed / 1000.0 maxSpeed = maxSpeed / 1000.0 power = power / 1000.0 brake = brake / 1000.0 friction = friction / 1000.0 rigidity = rigidity / 1000.0 -- impactTime is already in millisecond range -- turnAngle is initially seconds for 360° turn turnAngle = 360 / (turnAngle * 1000.0) -- Set private properties me.mSetEdgeTransformList() pLastUpdate = the milliseconds pThrust = 0 pTurn = 0 end beginSprite -- EVENT HANDLER -- on exitFrame(me) me.mUpdate() end exitFrame -- PRIVATE METHODS -- on mUpdate(me) ------------------------------------------------------- -- SENT BY stepFrame() if this instance is on the actorList or by -- exitFrame() if it is used as a behavior -------------------------------------------------------------------- -- Adjust for irregular frame durations tTime = the milliseconds pElapsedTime = tTime - pLastUpdate pLastUpdate = tTime if the shiftDown then -- Hold the camera still else me.mMoveCamera() end if tTopSpeed = me.mAvoidImpact() me.mDrive(tTopSpeed) end mUpdate on mAvoidImpact(me) -------------------------------------------------- -- SENT BY mUpdate() once per frame -- ACTION: Slows model if it comes directly up to an obstacle, or -- curves the model's trajectory so that it misses the -- obstacle. -------------------------------------------------------------------- -- Find out what direction we are currently travelling in tTransform = pilotModel.getWorldTransform() tDirection = -(tTransform.zAxis) -- Find the world position of the mid point of each side pLeftRay = (tTransform * pLeftEdge).position pRightRay = (tTransform * pRightEdge).position tHit = me.mGetHit(tDirection) if not listP(tHit) then -- no collision return maxSpeed end if -- Adjust tDistance so that it is the distance from front of -- cylinder to the point of impact. This ensures that no part of -- the cylinder will ever penetrate the obstacle, so that rotating -- the cylinder will never put the ray source on the other side of -- the wall. tDistance = max(0, tHit[#distance] - pRadius) if tDistance > (maxSpeed * impactTime) then -- Collision is beyond range of interest return maxSpeed end if if not speed or not tDistance then -- Don't interfere with turning else -- If the angle relative to the obstacle is great enough, increase -- the component of velocity parallel to the obstacle and reduce -- the component perpendicular to the obstacle so that collision -- never occurs. tNormal = tHit[#iSectNormal] -- Ignore vertical component of normal tNormal.y = 0 tNormal.normalize() if angleBetween(tDirection, -tNormal) < 20 then -- Allow pilot model to approach a wall directly else -- Reorient pilot model to avoid obstacle without slowing down if me.mAvoidObstacle(tDirection, tDistance, tNormal) then return maxSpeed end if end if end if return tDistance / impactTime end mAvoidImpact on mAvoidObstacle(me, aDirection, aDistance, aNormal) ---------------- -- SENT BY mAvoidImpact() if the pilot model is traveling under its -- own inertia towards an obstacle -- INPUT: is the current direction of the pilot model -- is the distance from the front of the bounding -- cylinder to the point of impact -- directional vector perpendicular to the face -- that modelsUnderRay() intersected -- ACTION: Maintains the component of velocity parallel to the -- obstacle and reduces the component perpendicular to the -- obstacle so that collision never occurs -------------------------------------------------------------------- -- Calculate perpendicular distance from pilot model to obstacle -- plane. Movement of this magnitude towards the obstacle plane -- must take at least impactTime. tPerpend = abs(aDirection * aNormal) * aDistance -- Calculate direction vector parallel to obstacle plane tParallel = aDirection * aDistance + aNormal * tPerpend tParallel.normalize() -- Calculate distance traveled at this speed in impactTime tDistance = speed * impactTime if tDistance < aDistance then -- The pilot model will not reach the obstacle by impactTime return FALSE end if -- Increase distance traveled parallel to obstacle plane in -- impactTime to compensate for the reduced distance perpendicular -- to obstacle plane tDistance = sqrt(tDistance * tDistance - tPerpend * tPerpend) -- Adopt orientation which will reach obstacle plane at impactTime tDisplacement = (tParallel * tDistance) - (aNormal * tPerpend) tRotation = pilotModel.transform.rotation pilotModel.pointAt(pilotModel.worldPosition + tDisplacement) -- Check that the new orientation does not create a closer impact tHit = me.mGetHit(tDisplacement) if listP(tHit) then if tHit[#distance] - pRadius < aDistance then -- Revert to previous orientation pilotModel.transform.rotation = tRotation return FALSE end if end if return TRUE -- pilot model was re-oriented end mAvoidObstacle on mGetHit(me, aDirection) -------------------------------------------- -- CALLED BY mAvoidImpact() and mAvoidObstacle() -- INPUT: is the direction in which the ray should fire -- OUTPUT: Shoots a ray forward from each edge as far as the first -- model and returns the data from the closest hit, or VOID -- if no models are encountered --------------------------------------------------------------------- tHit = pWorld.modelsUnderRay(pLeftRay, aDirection,1,#detailed) tRightHit = pWorld.modelsUnderRay(pRightRay,aDirection,1,#detailed) if tHit.count then tHit = tHit.getLast() tDistance = tHit[#distance] if tRightHit.count then -- There are potential collisions on both sides tRightHit = tRightHit.getLast() tRightDistance = tRightHit[#distance] if tDistance > tRightDistance then -- The collision on the right is closer tHit = tRightHit end if end if else if tRightHit.count then tHit = tRightHit.getLast() else -- no intersections return VOID tHit = VOID end if return tHit end mGetHit on mDrive(me, aTopSpeed) --------------------------------------------- -- SENT BY mUpdate() once per frame -- INPUT: is the maximum speed in units per millisecond, -- determined to prevent collision with other objects -- ACTION: Calculates how far the model has moved since the last -- update, and in what direction it is now moving. -------------------------------------------------------------------- -- Determine how speed has altered since the last update speed = max(0, min(aTopSpeed, speed + (pThrust * pElapsedTime))) -- Friction can be considered constant regardless of speed... speed = speed * (max(0, 1 - (friction * pElapsedTime))) -- ... except at low speeds if speed < minSpeed then if pThrust <= 0 then -- Stop the model if it is moving too slowly, with no power speed = 0 end if end if -- Determine how far the model has traveled. To be accurate, this -- requires an integration of a speed curve. To be kind to the CPU -- we will consider speed to be quantized. tDisplacement = -pElapsedTime * speed pilotModel.translate(0, 0, tDisplacement, #self) -- Adjust camera position ------------------------------------------ tVector = tDisplacement * pilotModel.getWorldTransform().zAxis tDistance = tVector.magnitude - closeUp if tDistance > 0 then tVector = tVector.getNormalized() * tDistance pCameraLoc = pCameraLoc + tVector * pFraction pCamera.worldPosition = pCameraLoc pCamera.pointAt(pilotModel) end if -------------------------------------------------------------------- -- Determine how far the model has turned since the last update. -- To be accurate, the model's path should have curved, but we will -- make it move in a series of tangents. Again, this will make the -- model turn faster on slower machines. tRotation = pElapsedTime * pTurn * turnAngle pilotModel.rotate(0, tRotation, 0, #self) -- Turn pTurn = keyPressed(123) - keyPressed(124) -- -1 | 0 | +1 -- Acceleration pThrust = (keyPressed(126) * power) - (keyPressed(125) * brake) end mDrive on mMoveCamera(me) --------------------------------------------------- -- SENT BY mDrive() if the Shift Key is not held down -- ACTION: Moves the camera elastically behind the pilot model. At -- slow frame rates, some elasticity is preserved by -- limiting the interpolation percentage to 80%. -------------------------------------------------------------------- tPosition = pilotModel.worldPosition tVector = tPosition - pCameraLoc tModels = pWorld.modelsUnderRay(pCameraLoc, tVector, 1) if tModels.getLast() = pilotModel then -- There are no obstacles between the camera and the pilot model pLastSight = 0 -- Move camera towards pilot model, but no closer than closeUp pFraction = max(0, min(rigidity * pElapsedTime, 1.0)) tDistance = tVector.magnitude - closeUp pVector = tVector.getNormalized() if tDistance < 0 then -- Don't move camera (but remember direction) else if the optionDown then nothing end if pVector = pVector * tDistance * pFraction pCameraLoc = pCameraLoc + pVector pCamera.worldPosition = pCameraLoc pCamera.pointAt(pilotModel) end if exit end if -- If we get here, then the camera has no clear view of the model if not pLastSight then -- The camera has just lost sight of the pilot model pLastSight = the milliseconds else if (the milliseconds - pLastSight) > 1000 then -- It's time to jump to a different camera position from which -- the pilot model can be seen me.mNewView() pLastSight = 0 exit end if -- Continue along the last known line of sight pVector = pVector * pFraction pCameraLoc = pCameraLoc + pVector pCamera.worldPosition = pCameraLoc if pCameraLoc <> tPosition then pCamera.pointAt(pilotModel) end if end mMoveCamera on mNewView(me) ------------------------------------------------------ -- SENT BY mMoveCamera() if the camera loses sight of the pilot -- model for more than a second -- ACTION: Places the camera in a new position from which the pilot -- model can be seen. Tries three postions, in turn: -- * closeUp behind the pilot model -- * mid-length shot from in front of the pilot model -- * first-person view from the model position -------------------------------------------------------------------- tTransform = pilotModel.getWorldTransform() tPosition = tTransform.position tAxis = tTransform.zAxis -- Try a closeUp view from behind pCameraLoc = me.mTestView(tPosition, tAxis) if voidP(pCameraLoc) then -- Try a mid-length view from in front pCameraLoc = me.mTestView(tPosition, -tAxis * 5) if voidP(pCameraLoc) then -- Use a first person view pCameraLoc = tPosition pCamera.transform = tTransform exit end if end if -- Adopt new view pCamera.worldPosition = pCameraLoc pCamera.pointAt(pilotModel) end mNewView on mTestView(me, aPosition, anAxis) ----------------------------------- -- CALLED by mNewView() -- INPUT: is the position of the pilot model -- is a vector indicating a direction from aPosition -- ACTION: Tests if the camera would have a clear view of the pilot -- model from a position determined by anAxis -- OUTPUT: a world position for the camera to adopt, if the view is -- clear, or VOID if the view is obstructed. -------------------------------------------------------------------- -- Look back from a pilot model tModels = pWorld.modelsUnderRay(aPosition, anAxis, 2, #detailed) if tModels.count then if tModels[1].model = pilotModel then tModels.deleteAt(1) end if if tModels.count then if tModels[1].distance < closeUp then -- There's an obstacle in the way return VOID end if end if end if tPosition = aPosition + anAxis * closeUp tModels = pWorld.modelsUnderRay(tPosition, -anAxis, 1) if tModels.getLast() = pilotModel then -- The view is clear return tPosition end if end mTestView on mSetEdgeTransformList(me) ----------------------------------------- -- SENT BY beginSprite(), new() -- ACTION: Calculates four transforms representing the relative -- positions of the four front corners of the pilot model's -- bounding box, and places them in pLeftEdge and pRightEdge -------------------------------------------------------------------- tResource = pilotModel.resource if tResource.type = #mesh then pRadius = me.mGetRadius(tResource.vertexList) else if (pilotModel.modifier).getPos(#meshDeform) then tRemoveMeshModifier = FALSE else pilotModel.addModifier(#meshDeform) tRemoveMeshModifier = TRUE end if pRadius = 0 tMeshCount = pilotModel.meshDeform.mesh.count repeat with i = 1 to tMeshCount tVertexList = pilotModel.meshDeform.mesh[i].vertexList pRadius = me.mGetRadius(tVertexList, pRadius) end repeat if tRemoveMeshModifier then pilotModel.removeModifier(#meshDeform) end if end if tTransform = transform() tTransform.position = vector(-pRadius, 0, 0) pLeftEdge = tTransform tTransform = transform() tTransform.position = vector(pRadius, 0, 0) pRightEdge = tTransform end mSetEdgeTransformList on mGetRadius(me, aVertexList, aRadius) ------------------------------ -- SENT BY mSetEdgeTransformList() -- -------------------------------------------------------------------- i = aVertexList.count repeat while i tVertex = aVertexList[i] tVertex.y = 0 tRadius = tVertex.magnitude if aRadius < tRadius then aRadius = tRadius end if i = i - 1 end repeat return aRadius end mGetRadius -- PUBLIC METHODS REQUIRED IF THIS SCRIPT IS NOT USED AS A BEHAVIOR -- -- The stepFrame(), new(), kill() and mSetParameters() handlers can be -- removed if this script is used as a behavior. -- -- If this script is not used as a behavior, the beginSprite(), -- exitFrame(), mTreatError(), mGetModelNames(me), isOKToAttach() and -- getPropertyDescriptionList() handlers can be removed instead. on stepFrame(me) me.mUpdate() end stepFrame on new(me, a3DSprite, aModel, aDataList) ----------------------------- -- -- INPUT: should be a 3D sprite or a property list with -- the structure: -- [#sprite: <3D sprite>, -- #model: <3D model or string model name>, -- #position: ] -- It may also contain any or all of the properties -- described for the aDataList property below. -- should be a 3D model in the member attached to -- a3DSprite. It may be if is a property -- list -- should be a position vector or a transform, -- defining the rest position of the camera relative to -- aModel -- may be a property list with the structure: -- [#speed: , -- #minSpeed: , -- #maxSpeed: , -- #power: , -- #brake: , -- #friction: , -- #rigidity: , -- #turnAngle: ] -- None of these properties are essential as default values -- will be used where any properties are missingor invalid. -------------------------------------------------------------------- -- Adopt the sprite's camera case ilk(a3DSprite) of #sprite: pWorld = a3DSprite.member if pWorld.type = #shockwave3D then pCamera = a3DSprite.camera else -- not a 3D sprite exit end if #propList: -- unpack the data from the list and try again aDataList = a3DSprite a3DSprite = aDataList[#sprite] aModel = aDataList[#model] aDataList.deleteProp(#sprite) aDataList.deleteProp(#model) return me.script.new(a3DSprite, aModel, aDataList) otherwise: -- not a sprite exit end case -- Adopt the anchor model case ilk(aModel) of #model: pilotModel = aModel #string: pilotModel = pWorld.model(aModel) if ilk(pilotModel) <> #model then exit -- not a valid model name end if otherwise -- not a model reference exit end case -- Set up max and min speeds, friction and so on if ilk(aDataList) <> #propList then aDataList = [:] end if me.mSetEdgeTransformList() me.mSetParameters(aDataList) pLastUpdate = the milliseconds pThrust = 0 return me end new on kill(me, anID) ---------------------------------------------------- -- SENT TO the actorList to destroy all instances of this script -- cleanly -- INPUT: is a pointer to the inheritor instance -- may be a symbol or a model reference -- ACTION: Removes this instance from the actorList if it -- corresponds to certain criteria: -- => If is then all instances on the -- actorList will be removed -- => If is #proximity, then only instances of this -- script will be removed from the actorList -- => If is pModelToTrack or pSensorModel then this -- particular instance will be removed from the actorList -------------------------------------------------------------------- case ilk(anID) of #symbol: if anID <> #camera then exit end if #camera: case anID of pCamera: -- remove instance otherwise: exit end case end case tIndex = (the actorList).getPos(me) if tIndex then -- Remove this instance from the actorList (the actorList).deleteAt(tIndex) if (the actorList).count >= tIndex then -- If the next instance on the actorList has a kill() handler -- the #kill event has to be sent to it manually. call(#kill, [(the actorList)[tIndex]]) end if end if end kill on mSetParameters(me, aDataList) ------------------------------------- -- SENT BY new() -- INPUT: will be a property list which may have one or -- more of the following properties: -- [#model: , -- #camera: , -- #closeUp: , -- #speed: , -- #maxSpeed: , -- #minSpeed: , -- #friction: , -- #power: = than 1.0>, -- #brake: , -- #turnAngle: , -- #impactTime: , -- #rigidity: ] -- NOTE: All figures are units per millisecond or per millisecond -- squared for accelerations. impactTime is in milliseconds -- ACTION: Adopts the custom parameter values passed in -- If any values are invalid, any parameters which have not -- already been set will be given valid default values. -------------------------------------------------------------------- -- Change pilot model tTemp = aDataList[#model] case ilk(tTemp) of #model: pilotModel = tTemp #integer, #string: tTemp = pWorld.model(tTemp) if ilk(tTemp, #model) then pilotModel = tTemp end if end case -- Change camera tTemp = aDataList[#camera] case ilk(tTemp) of #camera: pCamera = tTemp #integer, #string: tTemp = pWorld.camera(tTemp) if ilk(tTemp, #model) then pCamera = tTemp end if otherwise: -- Adopt the camera currently being used by this sprite pCamera = pSprite.camera end case -- Adopt a closeUp distance for the camera tTemp = aDataList[#closeUp] case ilk(tTemp) of #integer, #float: closeUp = max(1, tTemp) otherwise case ilk(closeUp) of #integer, #float: otherwise: closeUp = 50 end case end case -- All figures scaled to one millisecond. Values lower than -- 0.00001 may be considered to be zero in certain circumstances. tTemp = aDataList[#speed] case ilk(tTemp) of #integer, #float: speed = max(0, tTemp) -- otherwise: -- if voidP(speed) then -- speed = 0 -- end if end case -- Maximum speed may not be reached if friction is high with respect -- to power. 1.0 unit / millisecond = 1000 units /second -- (equivalent to the length of 20 standard box models) tTemp = aDataList[#maxSpeed] case ilk(tTemp) of #integer, #float: if tTemp > 0 then maxSpeed = tTemp else maxSpeed = 1.0 end if otherwise: if voidP(maxSpeed) then maxSpeed = 1.0 end if end case -- If the minimum speed is too high, the model may seem to stop -- abruptly tTemp = aDataList[#minSpeed] case ilk(tTemp) of #integer, #float: minSpeed = max(0, tTemp) otherwise: if voidP(minSpeed) then minSpeed = 0.01 end if end case -- Friction is applied every millisecond as follows: -- speed = speed * (1 - friction) tTemp = aDataList[#friction] case ilk(tTemp) of #integer, #float: friction = max(0, min(tTemp, 1)) otherwise: if voidP(friction) then friction = 0.0002 end if end case -- Power may be greater or less than brake. If it is greater, -- pressing both up and down arrows will result in slower -- acceleration. If it is less, pressing both keys will result in -- slower braking. tTemp = aDataList[#power] case ilk(tTemp) of #integer, #float: if tTemp > 0 then power = tTemp else power = 0.0004 end if otherwise: if voidP(power) then power = 0.0004 end if end case tTemp = aDataList[#brake] case ilk(tTemp) of #integer, #float: if tTemp > 0 then brake = tTemp else brake = 0.0003 end if otherwise: if voidP(power) then brake = 0.0003 end if end case -- 0.072° / millisecond => 5 seconds to complete a 360° turn tTemp = aDataList[#turnAngle] case ilk(tTemp) of #integer, #float: turnAngle = tTemp otherwise: if voidP(turnAngle) then turnAngle = 0.072 end if end case -- Elasticity of camera: lower values mean greater elasticity. If -- the value is too low, the pilot model may disappear from view. -- If it is too high the camera may appear to judder: at low frame -- rates all elasticity may be lost. The best value depends on a -- variety of factors such as power and friction. If the other -- default values are used, a value in the range of 0.1 <=> 0.9 is -- recommended. tTemp = aDataList[#rigidity] case ilk(tTemp) of #integer, #float: rigidity = max(0, min(tTemp, 100)) otherwise: if voidP(rigidity) then rigidity = 0.3 end if end case tTemp = aDataList[#impactTime] case ilk(tTemp) of #integer, #float: impactTime = max(0, min(tTemp, 2000)) otherwise: if voidP(impactTime) then impactTime = 500 end if end case return me end mSetParameters ---------------------------------------------------------------------- -- GPDL UTILITIES -- on mTreatError(me) --------------------------------------------------- -- SENT BY beginSprite() if the name entered for pilotModel is -- invalid -- ACTION: At authortime, shows an error alert, selects the -- offending sprite and halts the movie. At runtime, -- removes the behavior from the sprite. -------------------------------------------------------------------- if the runMode = "Author" then -- Determine the name of this behavior tName = string(me.script) put "member(" into word 1 of tName -- Determine where this sprite starts and ends tStart = pSprite.startFrame tEnd = pSprite.endFrame if tStart = tEnd then tFrame = "Frame: "&tStart else tFrame = "Frames: "&tStart&"-"&tEnd end if -- Show an informative alert alert \ "BEHAVIOR PARAMETER ERROR"&RETURN&\ "Sprite: "&spriteNum&", "&tFrame&RETURN&\ "Behavior: "&tName&RETURN&RETURN&\ "Model(""E&pilotModel"E&") not found."&RETURN&RETURN&\ "Please open the Parameters dialog for this behavior and choose a valid model name." -- Select the offending sprite and halt the movie the scoreSelection = [[spriteNum, spriteNum, tStart, tEnd]] halt else -- The movie is running in a browser or in a projector: remove -- this behavior from the sprite pSprite.scriptInstanceList.deleteOne(me) exit end if end mTreatError on mGetModelNames(me) ------------------------------------------------ -- CALLED by getPropertyDescriptionList() -- OUTPUT: A list of model names present in this 3D member. If the -- movie has not yet been run, this may be an empty list. -------------------------------------------------------------------- tModelNames = [] if the currentSpriteNum then tMember = sprite(the currentSpriteNum).member if tMember.type = #shockwave3D then tCount = tMember.model.count repeat with i = 1 to tCount tModelNames.append(tMember.model(i).name) end repeat end if end if return tModelNames end mGetModelNames -- BEHAVIOR DESCRIPTION AND PARAMETERS -- on iSOKToAttach(me, aSpriteType, aSpriteNumber) if aSpriteType = #graphic then return sprite(aSpriteNumber).member.type = #shockwave3D end if return FALSE end iSOKToAttach on getPropertyDescriptionList(me) tPropertyList = [:] tModelNames = me.mGetModelNames() if tModelNames.count then tPropertyList[ \ #pilotModel] = [ \ #comment: "Name of model piloted by this behavior", \ #format: #string, \ #range: tModelNames, \ #default: tModelNames[1] \ ] else tPropertyList[ \ #pilotModel] = [ \ #comment: "Name of model piloted by this behavior", \ #format: #string, \ #default: "Enter name of model here" \ ] end if tPropertyList[ \ #speed] = [ \ #comment: "Initial speed (world units per second)", \ #format: #integer, \ #range: [#min: 0, #max: 1000], \ #default: 0 \ ] tPropertyList[ \ #maxSpeed] = [ \ #comment: "Top speed (units per second)", \ #format: #integer, \ #range: [#min: 0, #max: 1000], \ #default: 500 \ ] tPropertyList[ \ #minSpeed] = [ \ #comment: "Lowest speed before complete stop (u/s)", \ #format: #integer, \ #range: [#min: 0, #max: 100], \ #default: 10 \ ] tPropertyList[ \ #friction] = [ \ #comment: "Friction", \ #format: #integer, \ #range: [#min: 0, #max: 100], \ #default: 2 \ ] tPropertyList[ \ #power] = [ \ #comment: "Acceleration effect with up arrow key (u/s/s)", \ #format: #integer, \ #range: [#min: 0, #max: 100], \ #default: 1 \ ] tPropertyList[ \ #brake] = [ \ #comment: "Braking effect with down arrow key (u/s/s)", \ #format: #integer, \ #range: [#min: 0, #max: 100], \ #default: 2 \ ] tPropertyList[ \ #turnAngle] = [ \ #comment: "Seconds to make a complete turn", \ #format: #float, \ #range: [#min: 0, #max: 60], \ #default: 5 \ ] tPropertyList[ \ #impactTime] = [ \ #comment: "Milliseconds required to reach an obstacle", \ #format: #integer, \ #range: [#min: 1, #max: 2000], \ #default: 500 \ ] tPropertyList[ \ #closeUp] = [ \ #comment: "Minimum distance from camera to piloted model", \ #format: #float, \ #range: [#min: 1.0, #max: 2000.0], \ #default: 50.0 \ ] tPropertyList[ \ #rigidity] = [ \ #comment: "Rigidity of camera movement", \ #format: #float, \ #range: [#min: 0.0, #max: 10.0], \ #default: 2 \ ] return tPropertyList end getPropertyDescriptionList