Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

...

breakoutModewide
languagec#

...

Goal: Learn how to control a crane to pickup and dropoff material

Video Tutorial: https://www.youtube.com/watch?v=6iCuejggoRI

Tip: Watch the video on YouTube in FullScreen mode and use HD quality (settings) to read the script clearly!

...

In this tutorial you will learn how to use a Crane to pickup a box and drop it somewhere else by combining learnings from the basic tutorials.

Sequence

The material handling steps is described in the swimlane diagram below. A spawner spawns a box on the InputSpline. When the box hits the Pickup-cue at the end of the spline, the statemachine sends the crane to the Pickup-cue. The box is parented to the crane and moved to the Dropoff-cue where it is unparented on the Dropoff-cue and given a MotionTensor. At the end of the OutputSpline the box is destroyed. The three Crane splines forms a MotionWeb that is created using the ActorMotionPathGraph tool. You will learn how to create this MotionWeb in another tutorial.

Scene

Fig.1 Scene diagram showing the Splines and Cues involved in the simulation.

Inc drawio
zoom1
simple0
pageId2099216385
custContentId2115567617
lbox1
diagramDisplayNameSplinesCues_CranePickupDropoff_v02_6dec2021.drawio
hiResPreview0
contentVer1
baseUrlhttps://unit040.atlassian.net/wiki
diagramNameSplinesCues_CranePickupDropoff_v02_6dec2021.drawio
pCenter0
aspectrbfaBNrf8GgwQ5zccoQI 1
width805
linksauto
tbstyletop
isUpload1
height301

State Diagram

Fig.2 State Diagram showing the flow from the Spawner on the InputSpline, via the states in the Crane state machine to the OutputSpline and Destroyer.

Inc drawio
zoom1
simple0
pageId2099216385
custContentId2116124846
lbox1
diagramDisplayNameStateDiagram_CranePickupDropoff_v05_6dec2021.drawio
hiResPreview0
contentVer1
baseUrlhttps://unit040.atlassian.net/wiki
diagramNameStateDiagram_CranePickupDropoff_v05_6dec2021.drawio
pCenter0
aspectWiLYc33vk6zyXuSaTwis 1
width491
linksauto
tbstyletop
isUpload1
height1809

Swimlane Diagram

Fig.3 Swimlane Diagram showing the resources used and events triggered during one material handling cycle.

Inc drawio
zoom1
simple0
pageId2099216385
custContentId2114781211
lbox1
diagramDisplayNameSwimdiagram_CranePickupDropoff_v07_6dec2021_kleur.drawio
hiResPreview0
contentVer1
baseUrlhttps://unit040.atlassian.net/wiki
diagramNameSwimdiagram_CranePickupDropoff_v07_6dec2021_kleur.drawio
pCenter0
aspectWiLYc33vk6zyXuSaTwis 1
width4001
linksauto
tbstyletop
isUpload1
height1222

Scene

The scene hierarchy consists of the following objects:

  • DES Controller: Controls the simulation and must be the parent of all simulation objects

  • InputSpline: Contains the Spawner and the cues for spawning and pickup (parenting).

  • Crane: Consists of a MotionWeb with the splines and cues for the base rotation, vertical movement and horizontal movement, and a CraneInstructor controlling the motion between the cues.

  • OutputSpline: Contains the cues for dropoff (unparenting) and destroying and the Destroyer.

  • BoxActor: Is the box (DESActor) that will be picked up.

...

Scripts

CraneInstructor Script

The CraneInstructor script detects boxes hitting the pickup cue on the InputSpline, controls the crane to pickup the box, rotates the crane to the dropoff position, drops the box and gives the box speed on the OutputSpline. Details are described below the code snippet.

Script 1 The CraneInstructor script that controls the actors in the MotionWeb

use the ChainInstructor.

Table of Contents

For convenience there is an example scene which can be used for this tutorial. The scene is called “5. Chain Simulation Start Scene“. This scene is provided with a DES Controller, some ChainLinkObjects, and shots to create a Chain Spline with.

Add ChainInstructor

Firstly, add a ChainInstructor to the scene. In the example scene, there is already a GameObject called “ChainInstructor“, so add the ChainInstructor component to this object. If using your own scene just add an GameObject and add a ChainInstructor component to it. Make sure to parent it under a DESContoller.

Create a ChainSpline

To describe the movement path of the chain, we have to create a Spline. This can be done in several ways, but the preferable one is using the Actor Route Creator. In the example scene, you can find some premade shots which can be used in combination with the Actor Route Creator to generate a Spline. Make sure the Spline is closed.

...

Prepare ChainLink prototypes (chain-links)

The example scene already provides meshes to use as individual chain-links (prototypes). Make sure to give the prototypes a ChainLink Component. Furthermore, we have to make sure to position the pivot of the prototypes to the wanted position and determine its “Leading” and “Trailing” space to be able to generate the chain later on. Example of how to determine these values are given in the picture below, using the example meshes from the example scene.

...

In the example scene, Situation 1 is used, with a trailing space of 0 m and a leading space of 0.1023874 m.

Generating the chain

Before we can generate the chain we have to set the values of the ChainInstructor. First we have to set the ChainSpline by selecting the created spline from the previous step. Secondly, we have to set the Chain Link Prototypes. Here we select our prototypes with their leading and trailing spaces. Lastly, we have to define a Chain Link Pattern. This is done by setting a List of integers, where 0, 1 and 2 correspond respectively to Element 0, Element 1 and Element 2 of the selected Chain Link Prototypes. Setting these fields for our example scene results in the following Inspector.

...

When we press the “Reinstantiate Chain Links”, this results in the following chain.

...

If we now press play and toggle the “Start Chain“ in the ChainInstructor inspector, we see we already have a moving chain.

...

Finetuning the chain

If we look closer, we see that the last chain link sequence is incomplete and not realistically linked with the starting link. To correct this, we have to make sure the length of the Chain Spline is a multiple of the sum of all the Leading and Trailing spaces in the Chain Link pattern. In the above case this will mean a multiple of (4 x 0.1023874 =) 0.4095496 meters. By first removing the previously generated chain by pressing the “Remove Chain Links“ button, we can alter the lengths of the straight sections of the Chain Spline. Moving spline point 1 to 8 -2.4966 m over the x-axis results for instance in the chain shown below.

...

Interacting with the chain

Interacting with the chain can be simply done by adding a Cue to the Chain Spline with an owner Instructor. Lets check the claim that the DES simulation can run with a precision of 1e-10 seconds. We create a cue somewhere on the Chain Spline and add the following instructor as owner instructor.

Code Block
using System.Collections.Generic;
using u040.prespective.prescripted.des.activities.motiontensor;
using u040.prespective.prescripted.des.activities.motiontensorbuffer;
using u040.prespective.prescripted.des.motionwebinstructors;
using u040.prespective.prescripted.des.participant.actor;
using u040.prespective.prescripted.des.participant.cue;
using u040.prespective.prescripted.des.supportUnityEngine;
using u040.prespective.prescripted.des.viewmodel;
using UnityEngine;

namespace u040.prespective.demos.cranePickupDropoff

public class ChainRoundTimeInstructor : DESInstructor
{
    //1) Class definitionpublic struct ParticipantPassingTime
    public{
class CraneInstructor : MotionWebInstructor     {public int ParticipantID;

     //2) MotionTensor of thepublic boxdouble afterTime;
dropoff
on the OutputSpline (define tensor in the Inspector) public ParticipantPassingTime(int _id, double _time)
   public MotionTensorViewModel OutputSplineTensor = new MotionTensorViewModel();{
         //3) Assignment buffer (storesParticipantID boxes= at_id;
the pickup point when the crane is busy)     Time = _time;
 public AssignmentBuffer CraneAssignmentBuffer       }
    }

    private List<ParticipantPassingTime> passingTimes = new AssignmentBufferList<ParticipantPassingTime>();

    public override bool ApplyInstruction(ADESActor _actor,  //===============================================================================================================
        // APPLY INSTRUCTION
        //===============================================================================================================

        //4) ApplyInstruction is called by the MotionWebInstructor when any DESActor crosses a DESCue (BoxActor/RotatorActor/VerticalActor/HorizontalActor)
        public override bool ApplyInstruction(ADESActor _actor, ADESCue _cue, ActorCueIntersectionEvent _intersectionEvent)
        {
            //5) Update the baseclass before calling the statemachine, otherwise the sequence will stop
            base.ApplyInstruction(_actor, _cue, _intersectionEvent);

            //6) Only accept center cue triggers (neglect enter and exit triggers)
            if (_intersectionEvent.IntersectionType != ActorCueIntersectionEvent.CueIntersectionType.Center)
            {
                //Return false, because not a breaking event. Exit ApplyInstruction.
                return false;
            }

            //7) Only accept BoxActor triggers (neglect Crane Horizontal/Vertical/Rotator actors)
            if (!(_actor is BoxActor))
            {
                //Return false, because not a breaking event. Exit ApplyInstruction.
                return false;
            }

            Debug.Log("Crane, go and pickup material..");

            //8) Call the RequestPickupByCrane method
            RequestPickupByCrane(_actor, _intersectionEvent.EventTime,
            new Func<double, MotionWebInstructionSequence, bool>((double _frameTimeAfterComplete, MotionWebInstructionSequence _justFinished) =>
            {
                //Return false, because not a breaking event, because the Crane has already finished it's sequence. Exit ApplyInstruction.
                return false;
            }));

            //Return false, because not a breaking event. Exit ApplyInstruction.
            return false;

        } //ApplyInstruction


        //===============================================================================================================
        // REQUEST PICKUP BY CRANE
        //===============================================================================================================

        //9) RequestPickupByCrane is called by ApplyInstruction when a box hits the pickup cue
        bool RequestPickupByCrane(ADESActor _thingToPickup, double _frameTimePassed, Func<double, MotionWebInstructionSequence, bool> _onDoneWithTransport)
        {
            //10) AssignmentBuffer (stores boxes at the Pickup cue if crane is busy)
            if (!CraneAssignmentBuffer.SetOrAddAssignment(_frameTimePassed,
                new BufferedAssignment()
                {
                    AssignmentDescription = "Pickup_Actor_" + _thingToPickup.ParticipantID,
                    AssignmentPriority = 99,
                    RequestingActor = _thingToPickup,
                    NumberOfRetries = -1,
                    TryStartAssignment = new Func<double, bool>((double _passedTimeAtRetry) => {
                        return RequestPickupByCrane(_thingToPickup, _passedTimeAtRetry, _onDoneWithTransport);
                    })
                }))
            {
                return true;
            }


            //11) Look up actors
            CraneRotatorMotionWebActor _rotator       = this.GetMotionWebActorsByType<CraneRotatorMotionWebActor>()[0];
            CraneVerticalMotionWebActor _vertical     = this.GetMotionWebActorsByType<CraneVerticalMotionWebActor>()[0];
            CraneHorizontalMotionWebActor _horizontal = this.GetMotionWebActorsByType<CraneHorizontalMotionWebActor>()[0];


            //PICKUP
            //12) OnArrivedAtPickup is called by the Statemachine
            Func<double, MotionWebInstructionStep, bool> OnArrivedAtPickupLocation = new Func<double, MotionWebInstructionStep, bool>(
                (double _passedFrameTimeAfterStep, MotionWebInstructionStep _stepJustCompleted) => 
                {
                    //Destroy the tensor on the box to be able to parent it to the Crane
                    List<MotionTensor> CurrentMotions = _thingToPickup.GetActivitiesByType<MotionTensor>();
                    for (int i = 0; i < CurrentMotions.Count; i++)
                    {
                        CurrentMotions[i].Destroy(_passedFrameTimeAfterStep, SimulationController);
                    }

                    //Parent box to the Crane (HorizontalActor)
                    _thingToPickup.ChangeTransformParent(_passedFrameTimeAfterStep, SimulationController.FrameTime, _horizontal);

                    //Return true, because parenting is a breaking event. Go to the next step in the statemachine.
                    return true;
                });


            //DROPOFF
            //13) OnArrivedAtDropOffLocation is called by the Statemachine
            Func<double, MotionWebInstructionStep, bool> OnArrivedAtDropOffLocation = new Func<double, MotionWebInstructionStep, bool>(
               (double _passedFrameTimeAfterStep, MotionWebInstructionStep _stepJustCompleted) => 
               {
                   //Unparent the box from the Crane ("null" means no new parent)
                   _thingToPickup.ChangeTransformParent(_passedFrameTimeAfterStep, SimulationController.FrameTime, null);

                   //Add a MotionTensor to the dropped box (speed etc. specified in the Inspector of the CraneInstructor)
                   if (OutputSplineTensor.TryGenerateMotionTensor(_thingToPickup, out MotionTensor _result))
                   {
                       SimulationController.AddActivityMidFrame(_passedFrameTimeAfterStep, SimulationController.FrameTime, _thingToPickup, _result);
                   }

                   //Return true, because unparenting is a breaking event. Go to the next step in the statemachine.
                   return true;
               });


            //FINISHED
            //14) DoOnDoneWithAssignement is called by the statemachine when it's finished
            Func<double, MotionWebInstructionSequence, bool> DoOnDoneWithAssignment = new Func<double, MotionWebInstructionSequence, bool>(
              (double _passedFrameTimeAfterAssignment, MotionWebInstructionSequence _completedSequence) =>
              {
                  //Pickup the next box in the AssignmentBuffer
                  CraneAssignmentBuffer.OnReadyForNextAssignment(_passedFrameTimeAfterAssignment);

                  //Return true to be sure event is picked up by the AssignmentBuffer
                  return true;
              });


            //15) STATEMACHINE
            MotionWebInstructionSequence _sequence = new MotionWebInstructionSequence()
            {
                SequenceID = "MoveCraneSequence",
                OnSequenceComplete = DoOnDoneWithAssignment,
                Verbose = false,
                InstructionSteps = new List<MotionWebInstructionStep>()
                {
                    //1: To VerticalTop
                    MotionWebInstructionStep.GetStepWithMotionVelocity(  
                        "1_VerticalTop",                                         //Name
                        .1d,                                                     //Velocity
                        _vertical,                                               //Actor
                        "CUE_VerticalTop"                                        //Target cue
                        ),

                    //2: To HorizontalIn
                    MotionWebInstructionStep.GetStepWithMotionVelocity(  
                        "2_HorizontalIn",                                         //Name
                        .1d,                                                      //Velocity
                        _horizontal,                                              //Actor
                        "CUE_HorizontalIn",                                       //Target cue
                        new List<string>(){ "1_VerticalTop" }                     //Previous step
                        ),

                    //3: To RotationPickup
                    MotionWebInstructionStep.GetStepWithMotionVelocity(  
                        "3_RotateToPickup",                                        //Name
                        .5d,                                                       //Velocity
                        _rotator,                                                  //Actor
                        "CUE_RotationPickup",                                      //Target cue
                        new List<string>(){ "2_HorizontalIn" }                     //Previous step
                        ),

                    //4: To HorizontalOut
                    MotionWebInstructionStep.GetStepWithMotionVelocity(  
                        "4_HorizontalOut",                                         //Name
                        .1d,                                                       //Velocity
                        _horizontal,                                               //Actor
                        "CUE_HorizontalOut",                                       //Target cue
                        new List<string>(){ "3_RotateToPickup" }                   //Previous step
                        ),
                      
                    //5: To VerticalBottom (PICKUP)
                    MotionWebInstructionStep.GetStepWithMotionVelocity(  
                        "5_VerticalBottom",                                        //Name
                        .1d,                                                       //Velocity
                        _vertical,                                                 //Actor
                        "CUE_VerticalBottom",                                      //Target cue
                        new List<string>(){ "4_HorizontalOut" },                   //Previous step
                        null,                                                      //Following step
                        null,                                                      //Midroute cue action
                        OnArrivedAtPickupLocation                                  //Run at TargetCue (pickup location)
                        ),

                    //6: To VerticalTop
                    MotionWebInstructionStep.GetStepWithMotionVelocity(  
                        "6_VerticalTop",                                           //Name
                        .1d,                                                       //Velocity
                        _vertical,                                                 //Actor
                        "CUE_VerticalTop",                                         //Target cue
                        new List<string>(){ "5_VerticalBottom" }                   //Previous step
                        ),

                    //7: To HorizontalIn
                    MotionWebInstructionStep.GetStepWithMotionVelocity(  
                        "7_HorizontalIn",                                          //Name
                        .1d,                                                       //Velocity
                        _horizontal,                                               //Actor
                        "CUE_HorizontalIn",                                        //Target cue
                        new List<string>(){ "6_VerticalTop" }                      //Previous step
                        ),

                    //8: To RotationDropoff
                    MotionWebInstructionStep.GetStepWithMotionVelocity(  
                        "8_RotateToDropoff",                                       //Name
                        .5d,                                                       //Velocity
                        _rotator,                                                  //Actor
                        "CUE_RotationDropoff",                                     //Target cue (one exit)
                        new List<string>(){ "7_HorizontalIn" }                     //Previous step
                        ),

                    //9: To HorizontalOut
                    MotionWebInstructionStep.GetStepWithMotionVelocity(  
                        "9_HorizontalOut",                                          //Name
                        .1d,                                                        //Velocity
                        _horizontal,                                                //Actor
                        "CUE_HorizontalOut",                                        //Target cue
                        new List<string>(){ "8_RotateToDropoff" }                   //Previous step
                        ),

                    //10: To VerticalBottom (DROPOFF)
                    MotionWebInstructionStep.GetStepWithMotionVelocity(  
                        "10_VerticalBottom",                                        //Name
                        .1d,                                                        //Velocity
                        _vertical,                                                  //Actor
                        "CUE_VerticalBottom",                                       //Target cue
                        new List<string>(){ "9_HorizontalOut" },                    //Previous step
                        null,                                                       //Following step
                        null,                                                       //Midroute cue action
                        OnArrivedAtDropOffLocation                                  //Run at dropoff location
                        ),

                    //11: To VerticalTop
                    MotionWebInstructionStep.GetStepWithMotionVelocity(
                        "11_VerticalTop",                                           //Name
                        .1d,                                                        //Velocity
                        _vertical,                                                  //Actor
                        "CUE_VerticalTop",                                          //Target cue
                        new List<string>(){ "10_VerticalBottom" }                   //Previous step
                        ),

                    //12: To HorizontalIn
                    MotionWebInstructionStep.GetStepWithMotionVelocity(
                        "12_HorizontalIn",                                          //Name
                        .1d,                                                        //Velocity
                        _horizontal,                                                //Actor
                        "CUE_HorizontalIn",                                         //Target cue
                        new List<string>(){ "11_VerticalTop" }                      //Previous step
                        ),

                    //13: To RotationPickup
                    MotionWebInstructionStep.GetStepWithMotionVelocity(
                        "13_RotateToPickup",                                       //Name
                        .5d,                                                       //Velocity
                        _rotator,                                                  //Actor
                        "CUE_RotationPickup",                                      //Target cue
                        new List<string>(){ "12_HorizontalIn" }                    //Previous step
                        )
                } //InstructionSteps
            }; //Sequence

            //16) Register the sequence
            InstructionSequences.Add(_sequence);

            //17) Start Statemachine/Sequence
            return _sequence.StartSequence(this, _frameTimePassed);
        
        } //RequestPickupByCrane
    } //class
} //namespace

CraneInstructor script Explanation

Line 14: public class CraneInstructor : MotionWebInstructor

The CraneInstructor is a MotionWebInstructor that makes it possible to use a Statemachine (Sequence) to control the Crane.

Line 17: public MotionTensorViewModel OutputSplineTensor = new MotionTensorViewModel();

The MotionTensorViewModel defines the MotionTensor for the box on the OutputSpline. The speed, MotionMode and MotionType is specified in the Inspector.

Line 20: public AssignmentBuffer CraneAssignmentBuffer = new AssignmentBuffer();

The AssignmentBuffer stores boxes arriving at the pickup point when the crane is busy.

Line 28: public override bool ApplyInstruction(ADESActor _actor, ADESCue _cue, ActorCueIntersectionEvent _intersectionEvent)

The ApplyInstruction method is mandatory for MotionWebInstructors and it is called whenever any of our DESActor's (BoxActor/RotatorActor/VerticalActor/HorizontalActor) crosses a DESCue connected to this CraneInstructor.

Line 31: base.ApplyInstruction(_actor, _cue, _intersectionEvent);

The base.ApplyInstruction calls and updates the baseclass to enable correct statemachine operation.

Line 34: if (_intersectionEvent.IntersectionType != ActorCueIntersectionEvent.CueIntersectionType.Center)

Only center cue hits are used (neglect enter and exit hits)

Line 41: if (!(_actor is BoxActor))

Only BoxActor hits are used (neglect Crane Horizontal/Vertical/Rotator actors hitting a cue)

Line 50: RequestPickupByCrane(_actor, _intersectionEvent.EventTime, new Func<...

Call the RequestPickupByCrane method defined next.

Line 68: bool RequestPickupByCrane(ADESActor _thingToPickup, double _frameTimePassed, Func<...

The RequestPickupByCrane method is called by ApplyInstruction when a box hits the pickup cue.

Line 71: if (!CraneAssignmentBuffer.SetOrAddAssignment(_frameTimePassed,
new BufferedAssignment()

An AssignmentBuffer is created that stores boxes hitting the Pickup cue while the crane is busy.

Line 88: CraneRotatorMotionWebActor _rotator = this.GetMotionWebActorsByType<CraneRotatorMotionWebActor>()[0];

The CraneRotatorMotionWebActor (3x) looks up the three actors in the MotionWeb and use those in the Statemachine sequence.

Line 95: Func<double, MotionWebInstructionStep, bool> OnArrivedAtPickupLocation = new Func<...

The OnArrivedAtPickupLocation function is called from the Statemachine when the crane arrives at the Pickup cue. The box tensor is destroyed and the box parented to the crane (horizontal actor). We return true because parenting is a breaking event.

Line 115: Func<double, MotionWebInstructionStep, bool> OnArrivedAtDropOffLocation = new Func<...

The OnArrivedAtDropOffLocation function is called from the Statemachine when the crane arrives with the box at the Dropoff cue. The box is unparented from the crane (parented to "null") and given a MotionTensor on the OutputSpline. We return true because parenting is a breaking event.

Line 134: Func<double, MotionWebInstructionSequence, bool> DoOnDoneWithAssignment = new Func<...

The DoOnDoneWithAssignment function is called from the Statemachine when it has finished all steps. A new assignment is requested from the AssignmentBuffer. We return true because starting a new assignment is a breaking event.

Line 146: MotionWebInstructionSequence _sequence = new MotionWebInstructionSequence()

The Statemachine is a sequence limited by the three splines and cues in the Crane MotionWeb. OnSequenceComplete specifies that the DoOnDoneWithAssignment function is called when the sequence is finished. Verbose enables detailed debug info in the Console. Each MotionWebInstructionStep defines the name of the target position, the velocity of the actor, the actor that will move, the name of the TargetCue, the name of the previous step and the function to run at the TargetCue.

Line 278: InstructionSequences.Add(_sequence);

The Add(_sequence) registers the statemachine.

Line 281: return _sequence.StartSequence(this, _frameTimePassed);

Finally the StartSequence starts the statemachine

BoxActor Script

Script 2 The BoxActor script defines this actor to be a DES Actor. The core of the script is empty.

Code Block
languagec#
using u040.prespective.prescripted.des.participant.actor;
using UnityEngine;

namespace u040.prespective.demos.cranePickupDropoff
{
    public class BoxActor : DESActor
    {

    }
}

CraneRotatorMotionWebActor Script

Script 3 The CraneRotatorMotionWebActor script defines this actor to be a MotionWebActor so it can move around in the MotionWeb. The core of the script is empty.

Code Block
languagec#
using u040.prespective.prescripted.des.motionweb;
using UnityEngine;

namespace u040.prespective.demos.cranePickupDropoff
{
    public class CraneRotatorMotionWebActor : MotionWebActor
    {

    }
}

CraneVerticalMotionWebActor Script

Script 4 The CraneVerticalMotionWebActor script defines this actor to be a MotionWebActor so it can move around in the MotionWeb. The core of the script is empty.

Code Block
languagec#
using u040.prespective.prescripted.des.motionweb;
using UnityEngine;

namespace u040.prespective.demos.cranePickupDropoff
{
    public class CraneVerticalMotionWebActor : MotionWebActor
    {

    }
}

CraneHorizontalMotionWebActor Script

The CraneHorizontalMotionWebActor script is also similar to the CraneRotatorMotionWebActor script.

Script 5 The CraneHorizontalMotionWebActor script that this actor to be a MotionWebActor so it can move around in the MotionWeb. The core of the script is empty.

Code Block
languagec#
using u040.prespective.prescripted.des.motionweb;
using UnityEngine;

namespace u040.prespective.demos.cranePickupDropoff
{
    public class CraneHorizontalMotionWebActor : MotionWebActor
    {

    }
}

Playmode

...

ADESCue _cue, ActorCueIntersectionEvent _intersectionEvent)
    {
        if (_intersectionEvent.IntersectionType != ActorCueIntersectionEvent.CueIntersectionType.Center || (!(_actor is ChainLink)))
        {
            return false;
        }

        double _currentSimTime = (SimulationController.TotalSimTimePassed + _intersectionEvent.EventTime);

        int timeIndex = passingTimes.FindIndex(_passingTime => _passingTime.ParticipantID == _actor.ParticipantID);

        if (timeIndex != -1)
        {
            ParticipantPassingTime passingTime = passingTimes[timeIndex];
            double _laptime = _currentSimTime - passingTime.Time;
            Debug.Log("Lap Time for Chain Actor [" + _actor.name + "] = " + _laptime);
            passingTimes.RemoveAt(timeIndex);
        }

        passingTimes.Add(new ParticipantPassingTime(_actor.ParticipantID, _currentSimTime));

        return false;
    }
}

After Pressing play and toggling the “Start Chain“ Button, we see the following logs in our console.

...

Comparing these values with the spline length of 6.55320689739471 (since the default chain velocity was 1 m/s), we see a precision of 1e-12.

Here we created a separate DESInstructor to link to the cue. To keep the chain instructor packed together you can also inherit from the ChainInstructor itself. Then you can use your own custom chain instructor to set things up. In this tutorial we chose to create a new separate instructor.

Optimizing performance

When making chains, you can quickly get a lot of chain link instances and thus a lot of DESActors in your scene. This can hamper the performance of your simulation. Not all chain links have to interact with something. For instance, you can determine that you actually only need interaction with the red links. So, to increase performance one can toggle on the “Transform Only“ options by the Chain Link Prototypes of the Chain Instructor for the wanted links. Make sure to reinstantiate the chain after altering the prototype settings. Toggle the Transformation Only for the inner and outer link prototypes, as shown below. If we now rerun the previous test, we only see the logs from the “Tow” links.

...

You can also change the Fixed Timestep of the Unity simulation to increase performance. It can be found under Edit → Project Settings → Time.