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!
https://www.youtube.com/watch?v=6iCuejggoRIIn 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.
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.
Swimlane Diagram
Fig.3 Swimlane Diagram showing the resources used and events triggered during one material handling cycle.
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
using System; using System.Collections.Generic; using u040.prespective.prescripted.des.activities.motiontensor; using u040.prespective.prescripted.des.motionweb; using u040.prespective.prescripted.des.participant.actor; using u040.prespective.prescripted.des.participant.cue; using u040.prespective.prescripted.des.support; using u040.prespective.prescripted.des.viewmodel; using UnityEngine; namespace u040.prespective.demos.cranePickupDropoff { //1) Class definition public class CraneInstructor : MotionWebInstructor { //2) MotionTensor of the box after dropoff on the OutputSpline (define tensor in the Inspector) public MotionTensorViewModel OutputSplineTensor = new MotionTensorViewModel(); //3) Assignment buffer (stores boxes at the pickup point when the crane is busy) public AssignmentBuffer CraneAssignmentBuffer = new AssignmentBuffer(); //=============================================================================================================== // 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.
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.
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.
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.
using u040.prespective.prescripted.des.motionweb; using UnityEngine; namespace u040.prespective.demos.cranePickupDropoff { public class CraneHorizontalMotionWebActor : MotionWebActor { } }
Playmode
Watch the video above to see what happens when we press play: The spawner spawns boxes on the InputSpline. When the first 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 via all the steps in the Statemachine. At the Dropoff-cue the box is unparented and given a MotionTensor on the OutputSpline. At the end of the OutputSpline, the box is destroyed.