...
breakoutMode | wide |
---|---|
language | c# |
...
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 | ||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
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 | ||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
Swimlane Diagram
Fig.3 Swimlane Diagram showing the resources used and events triggered during one material handling cycle.
Inc drawio | ||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
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.495 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 | ||
---|---|---|
| ||
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 | ||
---|---|---|
| ||
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 | ||
---|---|---|
| ||
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 | ||
---|---|---|
| ||
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.