Technical Document - Function Description - Simulator
Table of Contents
1. Overview
Simulator is a virtual environment for testing that allows you to easily check the operation of Unity Editor when developing applications for smart devices that communicate with toio™ Core Cube.
Directory structure looks like this
Assets/toio-sdk/Scripts/Simulator/ +------+ There's a script directly underneath
├── Editor/ +-----------------------------+ Unity Editor script to customize the Inspector
├── Materials/ +--------------------------+ Materials and physical materials used for objects in Simulator
├── Models/ +-----------------------------+ 3D models used for objects in Simulator
├── Prefabs/ +-------------------------+ Prefab is placed directly below it
└── AssetLoader/ +------------------------+ Various material files and loader scripts
├── Mat/ +----------------------------+ Various mat textures and materials
├── Ocatave/ +------------------------+ Sound files used for the Sound function
└── StandardID/ +---------------------+ Various standard ID textures
├── toio_collection/ +------------+ Toio Collection
└── simple_card/ +----------------+ Simple card
2. Mat Prefab
The Mat Prefab has the Mat.cs script and the MatAssetLoader.cs attached for loading materials.
Also, the Mat.cs inspector is customized by the script Editor/MatEditor.cs.
The following is a description of Mat.cs.
2.1. Conversion from mat coordinate units to meters
According to toio™ Core Cube Technical specifications / Communication specifications / Various functions / Reading sensor, the size of the playmat included with toio collection is 410 units in length and width.
The actual measurement of the mat was 56 cm = 0.560 m per side.
From here, we define the coordinate information of the mat and the factor DotPerM
to convert it to distance (meters) as follows
public static readonly float DotPerM = 411f/0.560f; // (410+1)/0.560 dot/m
2.2. Switching mat type
When you change the matType from the Inspector, the ApplyMatType method in Mat.cs will be executed to change the coordinate range and switch the material.
Switching is achieved by introducing the image into a Sprite texture, converting it from script to mesh, and replacing it in the SpriteRenderer.
Implementation code
public enum MatType
{
toio_collection_front = 0,
toio_collection_back = 1,
simple_playmat = 2,
developer = 3,
gesundroid = 4,
custom = 5 // Customize the coordinate range.
}
public MatType matType;
// Reflect changes in mat type and coordinate range.
internal void ApplyMatType()
{
// Resize
if (matType != MatType.custom)
{
var rect = GetRectForMatType(matType, developerMatType);
xMin = rect.xMin; xMax = rect.xMax;
yMin = rect.yMin; yMax = rect.yMax;
}
else
{
xMin = xMinCustom; xMax = xMaxCustom;
yMin = yMinCustom; yMax = yMaxCustom;
}
// Change material
var loader = GetComponent<MatAssetLoader>();
if (!loader) return;
Sprite sprite = loader.GetSprite(matType);
GetComponent<SpriteRenderer>().sprite = sprite;
// Create Mesh
var mesh = SpriteToMesh(sprite);
GetComponentInChildren<MeshFilter>().mesh = mesh;
// Update Mesh Collider
GetComponentInChildren<MeshCollider>().sharedMesh = null;
GetComponentInChildren<MeshCollider>().sharedMesh = mesh;
// Update size
var realW = (xMax-xMin)/DotPerM;
var realH = (yMax-yMin)/DotPerM;
var scaleW = sprite.pixelsPerUnit/(sprite.rect.width/realW);
var scaleH = sprite.pixelsPerUnit/(sprite.rect.width/realH);
this.transform.localScale = new Vector3(scaleW, scaleH, 1);
}
2.3. Converting the coordinates on the mat to the coordinates in Unity
We have a method to convert between coordinates/angles in Unity and coordinates/angles on the mat.
Mat Prefab can only be converted correctly if it is placed horizontally.
Implementation code
// Converting angles in Unity to angles on the main mat
public int UnityDeg2MatDeg(double deg)
{
return (int)(deg-this.transform.eulerAngles.y-90+0.49999f)%360;
}
// Convert the angle on this mat to the angle in Unity.
public float MatDeg2UnityDeg(double deg)
{
return (int)(deg+this.transform.eulerAngles.y+90+0.49999f)%360;
}
// Convert from Unity's 3D space coordinates to mat coordinates in this mat.
public Vector2Int UnityCoord2MatCoord(Vector3 unityCoord)
{
var matPos = this.transform.position;
var drad = - this.transform.eulerAngles.y * Mathf.Deg2Rad;
var _cos = Mathf.Cos(drad);
var _sin = Mathf.Sin(drad);
// Coordinate system shift: Match to this mat
var dx = unityCoord[0] - matPos[0];
var dy = -unityCoord[2] + matPos[2];
// Coordinate system rotation: Match the main mat
Vector2 coord = new Vector2(dx*_cos-dy*_sin, dx*_sin+dy*_cos);
// Convert to mat units
return new Vector2Int(
(int)(coord.x*DotPerM + this.xCenter + 0.4999f),
(int)(coord.y*DotPerM + this.yCenter + 0.4999f)
);
}
// Convert the mat coordinates in this mat to Unity's 3D space.
public Vector3 MatCoord2UnityCoord(double x, double y)
{
var matPos = this.transform.position;
var drad = this.transform.eulerAngles.y * Mathf.Deg2Rad;
var _cos = Mathf.Cos(drad);
var _sin = Mathf.Sin(drad);
// Convert to meters
var dx = ((float)x - xCenter)/DotPerM;
var dy = ((float)y - yCenter)/DotPerM;
// Coordinate system rotation: Match to Unity
Vector2 coord = new Vector2(dx*_cos-dy*_sin, dx*_sin+dy*_cos);
// Coordinate System Shift: Match Unity
coord.x += matPos.x;
coord.y += -matPos.z;
return new Vector3(coord.x, matPos.y, -coord.y);
}
3. StandardID Prefab
StandardID Prefab has the StandardID.cs script and the StandardIDAssetLoader.cs attached for loading materials.
Also, the inspector in StandardID.cs is customized by the script Editor/StandardIDEditor.cs.
The following is a description of StandardID.cs.
3.1. Switching between standard ID types
Switching StandardID types is achieved in the same approach as Mat.
Implementation code
internal void ApplyStandardIDType()
{
// Load Sprite
var loader = GetComponent<StandardIDAssetLoader>();
if (!loader) return;
Sprite sprite = null;
if (title == Title.toio_collection)
sprite = loader.GetSprite(toioColleType);
else if (title == Title.simple_card)
sprite = loader.GetSprite(simpleCardType);
GetComponent<SpriteRenderer>().sprite = sprite;
// Create Mesh
var mesh = SpriteToMesh(sprite);
GetComponentInChildren<MeshFilter>().mesh = mesh;
// Update Mesh Collider
GetComponentInChildren<MeshCollider>().sharedMesh = null;
GetComponentInChildren<MeshCollider>().sharedMesh = mesh;
// Update Size
float realWidth = 0.05f;
if (title == Title.toio_collection)
{
if ((int)toioColleType > 32) realWidth = 0.03f;
else if ((int)toioColleType < 21 || (int)toioColleType > 26) realWidth = 0.0575f;
else // Skunk
{
if (toioColleType == ToioColleType.id_skunk_blue) realWidth = 0.179f;
else if (toioColleType == ToioColleType.id_skunk_green) realWidth = 0.162f;
else if (toioColleType == ToioColleType.id_skunk_yellow) realWidth = 0.145f;
else if (toioColleType == ToioColleType.id_skunk_orange) realWidth = 0.1335f;
else if (toioColleType == ToioColleType.id_skunk_red) realWidth = 0.1285f;
else realWidth = 0.1225f; //toioColleType = ToioColleType.id_skunk_brown
}
}
else if (title == Title.simple_card)
{
if (simpleCardType == SimpleCardType.Full) realWidth = 0.297f;
else realWidth = 0.04f;
}
var scale = RealWidthToScale(sprite, realWidth);
this.transform.localScale = new Vector3(scale, scale, 1);
}
public static float RealWidthToScale(Sprite sprite, float realWidth)
{
return sprite.pixelsPerUnit/(sprite.rect.width/realWidth);
}
// http://tsubakit1.hateblo.jp/entry/2018/04/18/234424
private Mesh SpriteToMesh(Sprite sprite)
{
var mesh = new Mesh();
mesh.SetVertices(Array.ConvertAll(sprite.vertices, c => (Vector3)c).ToList());
mesh.SetUVs(0, sprite.uv.ToList());
mesh.SetTriangles(Array.ConvertAll(sprite.triangles, c => (int)c), 0);
return mesh;
}
4. Cube Prefab
There are three scripts implemented in Cube Prefab.
CubeSimulator.cs
:Implementation of a simulation of a real cube.CubeSimImpl.cs
:The base class for the implementation of each version of CubeSimulator.CubeSimImpl_v2_0_0.cs
:Implementation to support version 2.0.0CubeSimImpl_v2_1_0.cs
:Implementation to support version 2.1.0CubeSimImpl_v2_2_0.cs
:Implementation to support version 2.2.0
CubeSimulatorEditor.cs
:A customized version of theCubeSimulator.cs
inspectorCubeInteraction.cs
:Implemented operations of pushing and grabbing Cube objects on Simulator.AudioAssetLoader.cs
:Material loader
This chapter introduces the implementation of each version of CubeSimulator
.
4.1. Definition of constants
From the dimensions listed in toio™ Core Cube Technical specifications/Hardware specifications/Shape and size and Mat.DotPerM constants, the distance between the left and right tires and the size of Cube are defined as follows
// Distance between left and right tires (meters)
public static readonly float TireWidthM = 0.0266f;
// Distance between left and right tires (dots (mat coordinates))
public static readonly float TireWidthDot= 0.0266f * Mat.DotPerM;
// Cube Size
public static readonly float WidthM= 0.0318f;
Based on the motor specs listed in toio™ Core Cube Technical Specifications / Communication Specifications / Various Functions / Motors and the tire diameter (0.0125m) listed in toio™ Core Cube Technical specifications/Hardware specifications/Shape and size, The coefficients of the speed on the mat and the speed indication are defined as follows.
// Proportional to the speed (dots per second) and the indicated value
// (dot/s)/u = 4.3 rpm/u * pi * 0.0125m / (60s/m) * DotPerM
public static readonly float VDotOverU = 4.3f*Mathf.PI*0.0125f/60 * Mat.DotPerM; // about 2.06
4.2. Simulated state
Readout sensor
Raycast a ray “down” from the bottom of Cube, and if the object hit within 5mm is a Mat, get the Mat coordinates; if it is a StandardID, get the Standard ID If it is a StandardID, get the StandardID.
To get the mat coordinates, we use the Mat coordinate conversion method.
// CubeSimImpl_v2_0_0.cs
protected virtual void SimulateIDSensor()
{
// Simulate a reading sensor
// Simuate Position ID & Standard ID Sensor
RaycastHit hit;
Vector3 gposSensor = cube.transform.Find("sensor").position;
Ray ray = new Ray(gposSensor, -cube.transform.up);
if (Physics.Raycast(ray, out hit)) {
if (hit.transform.gameObject.tag == "Mat" && hit.distance < 0.005f){
var mat = hit.transform.gameObject.GetComponent<Mat>();
var coord = mat.UnityCoord2MatCoord(cube.transform.position);
var deg = mat.UnityDeg2MatDeg(cube.transform.eulerAngles.y);
var coordSensor = mat.UnityCoord2MatCoord(gposSensor);
var xSensor = coordSensor.x; var ySensor = coordSensor.y;
_SetXYDeg(coord.x, coord.y, deg, xSensor, ySensor);
}
else if (hit.transform.gameObject.tag == "StandardID" && hit.distance < 0.005f)
{
var stdID = hit.transform.gameObject.GetComponentInParent<StandardID>();
var deg = stdID.UnityDeg2MatDeg(cube.transform.eulerAngles.y);
_SetSandardID(stdID.id, deg);
}
else _SetOffGround();
}
else _SetOffGround();
}
The method _SetXYDeg
to set the Position ID and angle calls the callback IDCallback
if there are any changes.
// CubeSimImpl_v2_0_0.cs
protected void _SetXYDeg(int x, int y, int deg, int xSensor, int ySensor)
{
if (this.x != x || this.y != y || this.deg != deg || !this.onMat)
this.IDCallback?.Invoke(x, y, deg, xSensor, ySensor);
this.x = x; this.y = y; this.deg = deg;
this.xSensor = xSensor; this.ySensor = ySensor;
this.onMat = true;
this.onStandardID = false;
}
The method _SetStandardID
to set the Standard ID and angle calls the callback StandardIDCallback
if there are any changes.
// CubeSimImpl_v2_0_0.cs
protected void _SetSandardID(uint stdID, int deg)
{
if (this.standardID != stdID || this.deg != deg || !this.onStandardID)
this.standardIDCallback?.Invoke(stdID, deg);
this.standardID = stdID;
this.deg = deg;
this.onStandardID = true;
this.onMat = false;
}
The method _SetOffGround
will call the callbacks positionIDMissedCallback
or standardIDMissedCallback
if Cube leaves the Mat or StandardID.
// CubeSimImpl_v2_0_0.cs
protected void _SetOffGround()
{
if (this.onMat)
this.positionIDMissedCallback?.Invoke();
if (this.onStandardID)
this.standardIDMissedCallback?.Invoke();
this.onMat = false;
this.onStandardID = false;
}
Button
Calls the buttonCallback
callback when the button state is changed.
// CubeSimImpl_v2_0_0.cs
protected bool _button;
public override bool button
{
get {return this._button;}
internal set
{
if (this._button!=value){
this.buttonCallback?.Invoke(value);
}
this._button = value;
cube._SetPressed(value);
}
}
It also calls CubeSimulator._SetPressed
to represent Cube object being pressed.
// CubeSimulator.cs
internal void _SetPressed(bool pressed)
{
this.cubeModel.transform.localEulerAngles
= pressed? new Vector3(-93,0,0) : new Vector3(-90,0,0);
}
Horizontal detection
If the angle of Cube object exceeds the threshold, set sloped
to true.
// CubeSimImpl_v2_0_0.cs
protected virtual void SimulateMotionSensor()
{
// Horizontal detection
if (cube.isSimulateSloped)
{
cube.sloped = Vector3.Angle(Vector3.up, cube.transform.up)>45f;
}
...
}
Invoke the motion sensor callback via InvokeMotionSensorCallback
when sloped
is changed.
// CubeSimImpl_v2_0_0.cs
protected bool _sloped;
public override bool sloped
{
get {return this._sloped;}
internal set
{
if (this._sloped!=value){
this._sloped = value;
this.InvokeMotionSensorCallback();
}
}
}
Collision detection
Simulation of collision detection is not yet implemented.
When a collision is manually generated in the inspector, TriggerCollision
is called, which calls the motion sensor callback via InvokeMotionSensorCallback
.
// CubeSimImpl_v2_0_0.cs
protected bool _collisonDetected = false;
internal override void TriggerCollision()
{
this._collisonDetected = true;
this.InvokeMotionSensorCallback();
}
Double Tap
This is a feature of 2.1.0. Double-tap simulation is not yet implemented.
When a double-tap is pressed manually in the inspector, TriggerDoubleTap
is called, which calls the motion sensor callback via InvokeMotionSensorCallback
.
// CubeSimImpl_v2_1_0.cs
protected bool _doubleTapped = false;
internal override void TriggerDoubleTap()
{
this._doubleTapped = true;
this.InvokeMotionSensorCallback();
}
Attitude detection
This is a feature of 2.1.0. The principle is the same as the horizontal detection: if the angle of Cube object exceeds the threshold in the corresponding direction, the
pose
is set to the corresponding direction.
// CubeSimImpl_v2_1_0.cs
protected virtual void SimulateMotionSensor()
{
if(Vector3.Angle(Vector3.up, transform.up)<45f)
{
this.pose = Cube.PoseType.up;
}
else if(Vector3.Angle(Vector3.up, transform.up)>135f)
{
this.pose = Cube.PoseType.down;
}
else if(Vector3.Angle(Vector3.up, transform.forward)<45f)
{
this.pose = Cube.PoseType.forward;
}
else if(Vector3.Angle(Vector3.up, transform.forward)>135f)
{
this.pose = Cube.PoseType.backward;
}
else if(Vector3.Angle(Vector3.up, transform.right)<45f)
{
this.pose = Cube.PoseType.right;
}
else if(Vector3.Angle(Vector3.up, transform.right)>135f)
{
this.pose = Cube.PoseType.left;
}
}
Invoke the motion sensor callback through InvokeMotionSensorCallback
when pose
is changed.
// CubeSimImpl_v2_1_0.cs
protected Cube.PoseType _pose = Cube.PoseType.up;
public override Cube.PoseType pose {
get{ return _pose; }
internal set{
if (this._pose != value){
this._pose = value;
this.InvokeMotionSensorCallback();
}
}
}
Shake detection
This is a feature of 2.2.0. Simulation of shake detection is not yet implemented.
Invoke the motion sensor callback via InvokeMotionSensorCallback
when shakeLevel
is manually changed in the inspector.
// CubeSimImpl_v2_2_0.cs
protected int _shakeLevel;
public override int shakeLevel
{
get {return this._shakeLevel;}
internal set
{
if (this._shakeLevel != value){
this._shakeLevel = value;
this.InvokeMotionSensorCallback();
}
}
}
Motor speed detection
This is a feature of 2.2.0.
The tire speed calculated by the motor simulation is converted to the motor speed.
// CubeSimImpl_v2_2_0.cs
protected void SimulateMotorSpeedSensor()
{
int left = Mathf.RoundToInt(cube.speedTireL/CubeSimulator.VMeterOverU);
int right = Mathf.RoundToInt(cube.speedTireR/CubeSimulator.VMeterOverU);
_SetMotorSpeed(left, right);
}
Call the corresponding callback motorSpeedCallback
when the value is changed.
// CubeSimImpl_v2_2_0.cs
protected void _SetMotorSpeed(int left, int right)
{
left = Mathf.Abs(left);
right = Mathf.Abs(right);
if (motorSpeedEnabled)
if (this.leftMotorSpeed != left || this.rightMotorSpeed != right)
this.motorSpeedCallback?.Invoke(left, right);
this.leftMotorSpeed = left;
this.rightMotorSpeed = right;
}
Magnet state detection
This is a feature of 2.2.0.
CubeSimulator searches for the Magnet Prefab in the scene and calculates the composite magnetic field vector at the location of the magnetic sensor.
internal Vector3 _GetMagneticField()
{
if (isSimulateMagneticSensor)
{
var magnetObjs = GameObject.FindGameObjectsWithTag("t4u_Magnet");
var magnets = Array.ConvertAll(magnetObjs, obj => obj.GetComponent<Magnet>());
Vector3 magSensor = transform.Find("MagneticSensor").position;
Vector3 h = Vector3.zero;
foreach (var magnet in magnets)
{
h += magnet.SumUpH(magSensor);
}
this._magneticField = new Vector3(h.z, h.x, -h.y);
}
return this._magneticField;
}
Depending on the length and direction of the magnetic field vector, the magnet state transitions.
// CubeSimImpl_v2_2_0.cs
protected virtual void SimulateMagnetState(Vector3 force)
{
if (this.magneticMode != Cube.MagneticMode.MagnetState)
{
this.magnetState = Cube.MagnetState.None;
return;
}
var e = force.normalized;
var m = force.magnitude;
const float orientThreshold = 0.95f;
Cube.MagnetState state = this.magnetState;
if (m > 9000 && Vector3.Dot(e, Vector3.forward) > orientThreshold)
state = Cube.MagnetState.N_Center;
else if (m > 9000 && Vector3.Dot(e, Vector3.back) > orientThreshold)
state = Cube.MagnetState.S_Center;
else if (m > 6000 && Vector3.Dot(e, new Vector3(0, -1, 1).normalized) > orientThreshold)
state = Cube.MagnetState.N_Right;
else if (m > 6000 && Vector3.Dot(e, new Vector3(0, 1, 1).normalized) > orientThreshold)
state = Cube.MagnetState.N_Left;
else if (m > 6000 && Vector3.Dot(e, new Vector3(0, 1, -1).normalized) > orientThreshold)
state = Cube.MagnetState.S_Right;
else if (m > 6000 && Vector3.Dot(e, new Vector3(0, -1, -1).normalized) > orientThreshold)
state = Cube.MagnetState.S_Left;
else if (m < 200)
state = Cube.MagnetState.None;
_SetMagnetState(state);
}
Magnetic force detection
This is a feature of 2.3.0.
Convert the magnetic field vector to units for the cube.
// CubeSimImpl_v2_3_0.cs
protected virtual void SimulateMagneticForce(Vector3 force)
{
if (this.magneticMode != Cube.MagneticMode.MagneticForce)
{
this.magneticForce = Vector3.zero;
return;
}
force /= 450;
var orient = force.normalized * 10;
int ox = Mathf.RoundToInt(orient.x);
int oy = Mathf.RoundToInt(orient.y);
int oz = Mathf.RoundToInt(orient.z);
int mag = Mathf.RoundToInt(force.magnitude);
Vector3 f = new Vector3(ox, oy, oz);
f.Normalize();
f *= mag;
_SetMagneticForce(f);
}
Attitude detection
This is a feature from 2.3.0.
Converts a Cube Prefab from Euler angles in the Unity coordinate system to Euler angles in the coordinate system defined in the specification.
It also sets the Yaw reference value at startup and implements Yaw error accumulation.
// CubeSimulator.cs
private void _InitIMU()
{
this._attitudeYawBias = transform.eulerAngles.y;
}
private void _SimulateIMU()
{
this._attitudeYawBiasD += (UnityEngine.Random.value-0.5f) * 0.1f;
this._attitudeYawBiasD = Mathf.Clamp(this._attitudeYawBiasD, -1, 1);
this._attitudeYawBias += (this._attitudeYawBiasD + UnityEngine.Random.value-0.5f) * 0.01f;
}
internal Vector3 _GetIMU()
{
var e = transform.eulerAngles;
float roll = e.z;
float pitch = e.x;
float yaw = e.y - this._attitudeYawBias;
return new Vector3(roll, pitch, yaw);
}
The Euler angles and quaternions to be sent to the CubeUnity class are created by the Euler angles of the specification coordinate system. The range of Euler angles is adjusted and converted to a quaternion.
// CubeSimImpl_v2_4_0.cs
private float attitudeInitialYaw = 0;
protected virtual void SimulateAttitudeSensor()
{
var e = cube._GetIMU();
static int cvtInt(float f) { return (Mathf.RoundToInt(f) + 180) % 360 - 180; }
static float cvt(float f) { return (f + 180) % 360 - 180; }
Vector3 eulers;
if (this.attitudeFormat == Cube.AttitudeFormat.Eulers)
eulers = new Vector3(cvtInt(e.x), cvtInt(e.y), cvtInt(e.z));
else
eulers = new Vector3(cvt(e.x), cvt(e.y), cvt(e.z));
var quat = Quaternion.Euler(0,0,e.z) * Quaternion.Euler(0,e.y,0) * Quaternion.Euler(e.x,0,0);
_SetAttitude(eulers, quat);
}
4.3. Executing commands
Command processing flow
Simulator uses the following logic to process instructions passed from CubeUnity.
- When CubeUnity calls a method in CubeSimulator
- It starts a coroutine that calls the implementation method after a delay
- In the implementation method, the received instruction is held in the member variable “running instruction
- In FixedUpdate(), which is executed every frame, the following process is performed
- Execute the “instruction being executed.
- Clear the “executing instruction” when it finishes.
The delay parameter of Cube Prefab is set to the value actually measured in a real environment. It may vary depending on the device, environment, etc.
Motor
Use Raycast to investigate if the tires are hitting the ground.
// CubeSimulator.cs
internal bool offGroundL = true;
internal bool offGroundR = true;
private void SimulatePhysics_Input()
{
// Investigate tire landing conditions.
// Check if tires are Off Ground
RaycastHit hit;
var ray = new Ray(transform.position+transform.up*0.001f-transform.right*0.0133f, -transform.up); // left wheel
if (Physics.Raycast(ray, out hit) && hit.distance < 0.002f) offGroundL = false;
ray = new Ray(transform.position+transform.up*0.001f+transform.right*0.0133f, -transform.up); // right wheel
if (Physics.Raycast(ray, out hit) && hit.distance < 0.002f) offGroundR = false;
}
It converts the target speed of the current motor control instruction to the speed in Unity coordinate system, calculates the tire speed depending on if it is forced to stop or pushed, then calculates Cube speed depending on the landing state, and passes it to CubeSimulator._SetSpeed
.
// CubeSimulator.cs
private void SimulatePhysics_Output()
{
// Update tire speed.
if (this.forceStop || this.button || !this.isConnected) // Force stop
{
speedTireL = 0; speedTireR = 0;
}
else
{
var dt = Time.fixedDeltaTime;
speedTireL += (motorTargetSpdL - speedTireL) / Mathf.Max(this.motorTau, dt) * dt;
speedTireR += (motorTargetSpdR - speedTireR) / Mathf.Max(this.motorTau, dt) * dt;
}
// Get Cube's speed depending on its landing status
// update object's speed
// NOTES: simulation for slipping shall be implemented here
speedL = offGroundL? 0: speedTireL;
speedR = offGroundR? 0: speedTireR;
// Output
_SetSpeed(speedL, speedR);
}
Depending on the amount of change from the current speed to the target speed, Unity’s Rigidbody.Addforce will apply a force to the body, and the position and angle will be updated by Unity’s physics engine.
// CubeSimulator.cs
internal void _SetSpeed(float speedL, float speedR)
{
// Update position and angle by applying force through velocity change.
this.rb.angularVelocity = transform.up * (float)((speedL - speedR) / TireWidthM);
var vel = transform.forward * (speedL + speedR) / 2;
var dv = vel - this.rb.velocity;
this.rb.AddForce(dv, ForceMode.VelocityChange);
}
Further improvements (currently unimplemented items)
How to update velocity, position, and angle
AddForce, the frictional force of the mat is set to 0 and the delay factor, which is normally caused by the laws of physics, is included in the calculation of the target speed.
Since we are using a simplified model for these physical calculations, we cannot simulate the behavior of the mat when it is tilted.
For a more accurate modeling, the following steps could be considered:
- Input the difference between the target speed and the current speed converted from the motor control command into the same control module (e.g. PID) as the actual cube BLE protocol.
- Input the output “voltage” of the PID into the motor model.
- The “current” output of the motor model is converted to “power” and fed to the physics engine.
- Make the wheel’s collider, physics materials, etc. as realistic as possible.
Motor control with target specification
Since the BLE protocol implementation of the actual machine has not been released, the motor control with target specification of Simulator was implemented by referring to the specifications and the movements of the actual machine. There are some parts that were created by guessing, and there may be some differences from the actual machine, so we will explain some important parts.
Case where the move type is 0 (move while rotating)
In the case of rotate and move
, you decide whether to move forward or backward depending on whether the target is in front or behind Cube.
// CubeSimImpl_v2_1_0.cs
protected (float, float) TargetMove_MoveControl(float elipsed, ushort x, ushort y, byte maxSpd, Cube.TargetSpeedType targetSpeedType, float acc, Cube.TargetMoveType targetMoveType)
{
// ...
Vector2 targetPos = new Vector2(x, y);
Vector2 pos = new Vector2(this.x, this.y);
var dpos = targetPos - pos;
var dir2tar = Vector2.SignedAngle(Vector2.right, dpos);
var deg2tar = Deg(dir2tar - this.deg); // use when moving forward
var deg2tar_back = (deg2tar+360)%360 -180; // use when moving backward
bool tarOnFront = Mathf.Abs(deg2tar) <= 90;
// ...
switch (targetMoveType)
{
case (Cube.TargetMoveType.RotatingMove): // Rotate and move
{
rotate = tarOnFront? deg2tar : deg2tar_back;
translate = tarOnFront? spd : -spd;
break;
}
// ...
}
// ...
}
Case of motor speed change type with acceleration and deceleration
Taking the case of acceleration as an example, when the execution of a directive starts, the acceleration is calculated according to the length of the path and the maximum velocity. During the execution of the directive, the acceleration is based on the passage of time and acceleration, regardless of the position of Cube.
// CubeSimImpl_v2_1_0.cs
protected virtual void TargetMoveInit()
{
// ...
this.currMotorTargetCmd.acc = ((float)cmd.maxSpd*cmd.maxSpd-this.deadzone*this.deadzone) * CubeSimulator.VDotOverU/2/dist;
// ...
}
Steering Control
The rotation command value rotate
is calculated in proportion to the direction of travel and the angle to the target.
However, if you directly combine the rotate
with the translate
value (i.e., rotate
is proportional to the angular velocity of rotation), you will have insufficient rotation when the co-movement command value is large.
On the other hand, if you multiply rotate
and translate
to get a new rotate
value (i.e. rotate
is proportional to the radius of rotation), you will have under-rotation when the co-movement command is small.
So, depending on the size of the translate
, we can take a weighted average of the above two types of rotate
to eliminate the lack of rotation.
// CubeSimImpl_v2_1_0.cs
protected void ApplyMotorControl(float translate, float rotate)
{
var miu = Mathf.Abs(translate / this.maxMotor);
rotate *= miu * Mathf.Abs(translate/50) + (1-miu) * 1;
var uL = translate + rotate;
var uR = translate - rotate;
// ...
}
Sound
We are using Unity’s AudioSource component to play a tone according to a MIDI note number.
Generating a reference sound source
The tones of A (A0 to A10) in each octave are created as audio files in advance.
The audio is generated by the following python script, which generates a wav file that samples a sine wave of one period.
Implementation code
import numpy as np
import wave
import struct
nsamples = 32 # samples in 1 period
sin_array = [int(-np.cos(2*np.pi*i/nsamples)*127) for i in range(nsamples)]
f_A0 = 440/16
duration = 0.0233 # Since Unity 2020, audio shorter than this will not be imported correctly
for i in range(11):
f = f_A0 * 2**i
T = 1/f
audio_array = sin_array * np.ceil(duration/T).astype(int)
audio = struct.pack("b" * len(audio_array), *audio_array)
w = wave.Wave_write(str(12*i+9) + '.wav')
p = (1, 1, nsamples*f, len(audio), 'NONE', 'not compressed')
w.setparams(p)
w.writeframes(audio)
w.close()
This audio file is named according to the correspondence table in toio™ Core Cube Technical Specifications/Communication Specifications/Functions/Sounds and placed in [Assets/toio-sdk/Scripts/Simulator/AssetLoader/Octave].
Playing sounds
Scales other than A, which are prepared in advance, are converted and played back from A in the same octave using the Pitch parameter of AudioSource.
// CubeSimulator.cs
private int playingSoundId = -1;
internal void _PlaySound(int soundId, int volume){
if (soundId >= 128) { _StopSound(); return; }
if (soundId != playingSoundId)
{
playingSoundId = soundId;
int octave = (int)(soundId/12);
int idx = (int)(soundId%12);
var loader = GetComponent<AudioAssetLoader>();
if (!loader) return;
audioSource.clip = loader.GetAudioCLip(octave);
audioSource.pitch = (float)Math.Pow(2, ((float)idx-9)/12);
}
audioSource.volume = (float)volume/256 * 0.5f;
if (!audioSource.isPlaying)
audioSource.Play();
}
Lamp
Placing a light source in the lamp to represent the luminescence would make the process too heavy, so we simply change the color of the material.
// CubeSimulator.cs
internal void _SetLight(int r, int g, int b){
LED.GetComponent<Renderer>().material.color = new Color(r/255f, g/255f, b/255f);
}
5. Stage Prefab
Stage Prefab is a set of the following
- Mat Prefab
- Camera (including physical raycaster)
- Light
- Target pole
- Tables and borders to prevent Cube from escaping
- EventSystem
5.1. Target pole
By right-clicking or dragging the mouse, target poles can be placed and moved. The developer can get the position of the target pole and use it to control Cube.
Implementation code
void Update(){
// Move the target pole
// Moving TargetPole
if (isDragging)
{
RaycastHit hit;
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
if (Physics.Raycast(ray, out hit) && targetPole != null) {
targetPole.position = new Vector3(hit.point.x, targetPole.position.y, hit.point.z);
}
}
...
}
The property tarPoleCoord
can be used to get the coordinates of the target pole on the mat, which is useful when moving Cube.
5.2. Focus on Cube
When you left-click, the ray will fly from the mouse cursor position and follow Cube that the ray collided with, focusing the spotlight on it.
Implementation code
void Update(){
...
// Keep focusing on focusTarget
if (focusTarget!=null){
var tar = new Vector3(0, 0.01f, 0) + focusTarget.position;
mainLightObj.GetComponent<Light>().transform.LookAt(tar);
sideLightObj.GetComponent<Light>().transform.LookAt(tar);
}
...
}
private void OnLeftDown()
{
var camera = Camera.main;
RaycastHit hit;
Ray ray = camera.ScreenPointToRay(Input.mousePosition);
if (Physics.Raycast(ray, out hit)) {
if (hit.transform.gameObject.tag == "Cube")
SetFocus(hit.transform);
else SetNoFocus();
}
else SetNoFocus();
}
The property focusName
allows you to get the name of Cube to focus on.
When debugging a process with a large number of Cubes, it is useful to check the behavior of each individual Cube.
6. Magnet Prefab
he Magnet Prefab has the script Magnet.cs attached to it.
The script Magnet.cs is also attached to the child objects that represent the magnetic load contained in the Magnet Prefab, but
However, only the parent object Magnet has the tag t4u_Magnet
, so CubeSimulator recognizes only the parent object as a single magnet.
Magnet.cs can calculate the vector that the magnetic field defined by itself will place at a given position.
public Vector3 GetSelfH(Vector3 pos)
{
var src = transform.position;
var dpos = pos - src;
var r = dpos.magnitude;
if (r > maxDistance) return Vector3.zero;
return maxwell * 10e-8f / (4 * Mathf.PI * mu * r * r * r) * dpos;
}
The synthetic magnetic field defined by Magnet.cs, which is attached to the Magnet Prefab parent object and all child objects, is recursively sought.
public Vector3 SumUpH(Vector3 pos)
{
if (Vector3.Distance(pos, transform.position) > maxDistance) return Vector3.zero;
var magnets = GetComponentsInChildren<Magnet>();
Vector3 h = Vector3.zero;
foreach (var magnet in magnets)
{
h += magnet.GetSelfH(pos);
}
return h;
}