某愿朝闻道译/原文地址

  • 同时生成多个形状
  • 环绕另一个形状做轨道运动
  • 持续追踪特定的形状实例
  • 限制形状总数

这是对象管理章节的第十篇教程. 将在上个教程的代码基础上制作. 本篇教程将让形状之间产生联系, 添加一种类似卫星运动的行为

本教程源码使用Unity2017.4.12f1编写
翻译中 : 形造卫星 - 图1
体积更大的形状更有魅力, 吸引了小一些的形状

生成多个形状

本篇教程将赋予形状环绕其他形状做轨道运动的行为, 就像一个卫星. 形状生成时决定它是否拥有卫星. 如果有, 则同时生成它的卫星形状. 也就是说每次生成形状, 都可能得到两个而不是一个形状

为每个形状生成卫星

要生成卫星, 需要在SpawnZone脚本添加新方法CreateSatelliteFor, 该方法接受一个Shape类型的参数. 该参数就是要卫星形状要环绕运动的母星形状, 我们将使用一个随机工厂创建卫星形状, 然后为它赋予一个随机的旋转朝向 :

  1. void CreateSatelliteFor (Shape focalShape) {
  2. int factoryIndex = Random.Range(0, spawnConfig.factories.Length);
  3. Shape shape = spawnConfig.factories[factoryIndex].GetRandom();
  4. Transform t = shape.transform;
  5. t.localRotation = Random.rotation;
  6. }

至此还没有真正的创建出一个卫星. 现在我们要让卫星形状的尺寸设置为焦点形状的一半, 位置在其之上一个单位. 并且向上运动 :

  1. t.localRotation = Random.rotation;
  2. t.localScale = focalShape.transform.localScale * 0.5f;
  3. t.localPosition = focalShape.transform.localPosition + Vector3.up;
  4. shape.AddBehavior<MovementShapeBehavior>().Velocity = Vector3.up;

我们还要给卫星形状设置颜色, 颜色设置的代码与之前在SpawnShape中实现的代码一样, 所以我们将之前的颜色代码单独放在一个新的方法SetupColor中来复用.

  1. public virtual Shape SpawnShape () {
  2. //…
  3. //if (spawnConfig.uniformColor) {
  4. // shape.SetColor(spawnConfig.color.RandomInRange);
  5. //}
  6. //else {
  7. // …
  8. //}
  9. SetupColor(shape);
  10. //…
  11. }
  12. void CreateSatelliteFor (Shape focalShape) {
  13. //…
  14. SetupColor(shape);
  15. }
  16. void SetupColor (Shape shape) {
  17. if (spawnConfig.uniformColor) {
  18. shape.SetColor(spawnConfig.color.RandomInRange);
  19. }
  20. else {
  21. for (int i = 0; i < shape.ColorCount; i++) {
  22. shape.SetColor(spawnConfig.color.RandomInRange, i);
  23. }
  24. }
  25. }

通过在SpawnShape的末尾调用CreateSatteliteFor方法, 来为每个形状都赋予一个卫星伙伴, 也就是让形状们成双成对的出现.

  1. public virtual Shape SpawnShape () {
  2. //…
  3. CreateSatelliteFor(shape);
  4. return shape;
  5. }

**

在游戏中加入形状

SpawnShape方法的目的是向游戏中加入新形状, 它返回一个Shape类型的对象, 这样Game类就可以将这个返回对象加入到形状列表中. 但是对于我们的卫星形状, 目前为止没有将它加入到形状列表中, 因此微信形状不会被更新状态, 也不会暂停.

我们可以让SpawnShape返回一个形状列表, 但是我们的目标是无论一个形状在何时何地加入到了游戏中, 都应该将其放入形状列表中. 我们可以将这个责任转移给Game的静态实例, 然后只需要关注是谁产生了一个形状并将它传递给了Game.

  1. public static Game Instance { get; private set; }
  2. //…
  3. void OnEnable () {
  4. Instance = this;
  5. //…
  6. }

为了接收新的形状, 在Game类中添加一个新的公开方法AddShape, 用于向形状列表中加入形状.

  1. public void AddShape (Shape shape) {
  2. shapes.Add(shape);
  3. }

我们将使用ShapeFactory.Get方法来将每个形状添加到Game中. 这样做使得ShapeFactory能够觉察到Game的存在, 如此一来, 我们可以无需操心一个形状在何时被添加到Game中.

  1. public Shape Get (int shapeId = 0, int materialId = 0) {
  2. //…
  3. Game.Instance.AddShape(instance);
  4. return instance;
  5. }

对于Game.LoadGame方法来说, 现在就不需要在它的内部将加载的形状对象添加到列表中了, 否则会导致形状被添加两次.

  1. IEnumerator LoadGame (GameDataReader reader) {
  2. //…
  3. for (int i = 0; i < count; i++) {
  4. int factoryId = version >= 5 ? reader.ReadInt() : 0;
  5. int shapeId = version > 0 ? reader.ReadInt() : 0;
  6. int materialId = version > 0 ? reader.ReadInt() : 0;
  7. Shape instance = shapeFactories[factoryId].Get(shapeId, materialId);
  8. instance.Load(reader);
  9. //shapes.Add(instance);
  10. }
  11. }

产生任意数量的形状

此时SpawnZone.SpawnShape方法的代码已经不太适合了. 首先它不在需要返回一个形状对象. 其次, 它不应该仅能返回一个单独的形状, 因为它现在每次调用的时候会产生两次形状. 因此我们将它的返回值类型改为void:

  1. //public virtual Shape SpawnShape () {
  2. public virtual void SpawnShapes () {
  3. int factoryIndex = Random.Range(0, spawnConfig.factories.Length);
  4. //…
  5. CreateSatelliteFor(shape);
  6. //return shape;
  7. }

对于CompositeSpawnZone类做出类似的修改 :

  1. //public override Shape SpawnShape () {
  2. public override void SpawnShapes () {
  3. if (overrideConfig) {
  4. //return
  5. base.SpawnShapes();
  6. }
  7. else {
  8. //…
  9. //return
  10. spawnZones[index].SpawnShapes();
  11. }
  12. }

同样需要做出调整的是GameLevel中的SpawnShape方法:

  1. //public Shape SpawnShape () {
  2. public void SpawnShapes () {
  3. //return
  4. spawnZone.SpawnShapes();
  5. }

至此, 可以移除Game.CreateShape方法, 取而代之的方式就是在Update和FixedUpdate方法中直接调用GameLevel.Current.SpawnShapes :

  1. void Update () {
  2. if (Input.GetKeyDown(createKey)) {
  3. //CreateShape();
  4. GameLevel.Current.SpawnShapes();
  5. }
  6. //…
  7. }
  8. void FixedUpdate () {
  9. //…
  10. while (creationProgress >= 1f) {
  11. creationProgress -= 1f;
  12. //CreateShape();
  13. GameLevel.Current.SpawnShapes();
  14. }
  15. //…
  16. }
  17. //…
  18. //void CreateShape () {
  19. // shapes.Add(GameLevel.Current.SpawnShape());
  20. //}

SomberGeneralLacewing-mobile.mp4 (281.06KB)成对出现的形状(那些垂直向上运动的就是我们设置的卫星形状)

卫星行为

想要让这些伴随其他形状而生的形状更符合”卫星”的特点, 我们需要为这一类形状创建一个新的行为类型.

新的形状行为

在ShapeBehaviorType枚举中加入一个新的项目Satellite, 当在GetInstance方法的Switch语句中, 将在监测到该枚举类型时返回一个SatelliteShapeBehavior类型的实例:

  1. public enum ShapeBehaviorType {
  2. Movement,
  3. Rotation,
  4. Oscillation,
  5. Satellite
  6. }
  7. public static class ShapeBehaviorTypeMethods {
  8. public static ShapeBehavior GetInstance (this ShapeBehaviorType type) {
  9. switch (type) {
  10. //…
  11. case ShapeBehaviorType.Satellite:
  12. return ShapeBehaviorPool<SatelliteShapeBehavior>.Get();
  13. }
  14. UnityEngine.Debug.Log("Forgot to support " + type);
  15. return null;
  16. }
  17. }

然后创建一个名为StaelliteShapeBehavior的组件, 代码如下 :

  1. using UnityEngine;
  2. public sealed class SatelliteShapeBehavior : ShapeBehavior {
  3. public override ShapeBehaviorType BehaviorType {
  4. get {
  5. return ShapeBehaviorType.Satellite;
  6. }
  7. }
  8. public override void GameUpdate (Shape shape) {}
  9. public override void Save (GameDataWriter writer) {}
  10. public override void Load (GameDataReader reader) {}
  11. public override void Recycle () {
  12. ShapeBehaviorPool<SatelliteShapeBehavior>.Reclaim(this);
  13. }
  14. }

将该行为在SpawnZone.CreateSatelliteFor中添加给形状, 并且移除删除之前用来测试的运动与位置代码 :

  1. void CreateSatelliteFor (Shape focalShape) {
  2. //…
  3. //t.localPosition = focalShape.transform.localPosition + Vector3.up;
  4. //shape.AddBehavior<MovementShapeBehavior>().Velocity = Vector3.up;
  5. SetupColor(shape);
  6. shape.AddBehavior<SatelliteShapeBehavior>();
  7. }

卫星配置

对于卫星形状的产生, 也需要可以在生成区对象的Inspector窗口中对它们进行配置. 在SpawnConfiguration中定义一个名为SatelliteConfiguration的结构, 目前我们将利用它控制卫星的缩放比例, 在Inspector窗口中, 它的配置范围设置为0.1到1之间 :

  1. [System.Serializable]
  2. public struct SpawnConfiguration {
  3. //…
  4. [System.Serializable]
  5. public struct SatelliteConfiguration {
  6. [FloatRangeSlider(0.1f, 1f)]
  7. public FloatRange relativeScale;
  8. }
  9. public SatelliteConfiguration satellite;
  10. }

翻译中 : 形造卫星 - 图3
在Inspector窗口中的Relative Scale
接着用一个随机范围代替固定的0.5来设置卫星的相对缩放大小:

  1. t.localScale = focalShape.transform.localScale * spawnConfig.satellite.relativeScale.RandomValueInRange;

我们还需要一个半径来控制卫星与母星之间的距离, 一个运行频率来控制卫星的运转周期 :

  1. public struct SatelliteConfiguration {
  2. [FloatRangeSlider(0.1f, 1f)]
  3. public FloatRange relativeScale;
  4. public FloatRange orbitRadius;
  5. public FloatRange orbitFrequency;
  6. }

翻译中 : 形造卫星 - 图4
卫星的半径和频率
这些配置数据需要应用到特定的卫星形状上来生效, 我们不需要再SpawnZone中书写这部分代码, 而是在SatelliteShapeBehavior中添加一个Initialize方法, 将与卫星有关的数据作为参数传递给它:

  1. public void Initialize (Shape shape, Shape focalShape, float radius, float frequency)
  2. {}

然后就可以在SpawnZone中添加卫星行为时对其进行初始化:

  1. void CreateSatelliteFor(Shape focalShape)
  2. {
  3. //...
  4. //shape.AddBehavior<SatelliteShapeBehavior>();
  5. shape.AddBehavior<SatelliteShapeBehavior>().Initialize(shape, focalShape,spawnConfig.satellite.orbitRadius.RandomValueInRange,spawnConfig.satellite.orbitFrequency.RandomValueInRange);
  6. }

轨道运行

要让卫星围绕母星旋转, 需要用到三角学, 通过形状寿命沿着两个正交矢量的正弦和余弦来便宜卫星的位置. 要做到这一点需要使得SatelliteShapeBehavior能够追踪母星形状, 运行频率, 两个偏移向量. 半径可以作为偏移的系数:
我们从X轴的余弦偏移和Z轴的正弦偏移开始. 如果从上方向下看, 我们会看到卫星环绕母星做逆时针旋转:

  1. Shape focalShape;
  2. float frequency;
  3. Vector3 cosOffset, sinOffset;
  4. public void Initialize (Shape shape, Shape focalShape, float radius, float frequency)
  5. {
  6. this.focalShape = focalShape;
  7. this.frequency = frequency;
  8. cosOffset = Vector3.right;
  9. sinOffset = Vector3.forward;
  10. cosOffset *= radius;
  11. sinOffset *= radius;
  12. }

要让卫星运动起来, 我们需要在SatelliteShapeBehavior.GameUpate中调整它的位置. 将它的位置在母星位置的基础上进行偏移, 偏移值被卫星寿命的2π 倍的正弦或余弦来缩放:

  1. public override void GameUpdate (Shape shape) {
  2. float t = 2f * Mathf.PI * frequency * shape.Age;
  3. shape.transform.localPosition = focalShape.transform.localPosition + cosOffset * Mathf.Cos(t) + sinOffset * Mathf.Sin(t);
  4. }

为了确保卫星的初始位置是正确的, 需要在Initialize方法的末尾处再调用一次GameUpdate方法, 这是因为GameUpdate方法不会自动的在形状被生成的那一帧被执行 :

  1. public void Initialize (Shape shape, Shape focalShape, float radius, float frequency)
  2. {
  3. //…
  4. GameUpdate(shape);
  5. }

SomberGeneralLacewing-mobile.mp4 (438.45KB)卫星环绕运动

椭圆轨道也能实现吗?

椭圆轨道也是可能的, 但比圆形轨道更复杂. 通过为每个偏移使用不同的半径, 可以将轨道变成椭圆. 除此之外, 轨道速度应该不再是恒定的, 而是取决于卫星母星之间的距离.

随机平面的轨道

目前的卫星轨道处于XZ平面内, 环绕Y轴进行运动. 我们可以通过Random.onUnitSphere来让轨道环绕随机轴 :

  1. Vector3 orbitAxis = Random.onUnitSphere;
  2. cosOffset = Vector3.right;
  3. sinOffset = Vector3.forward;

获得了随机轴后, 下一步就是找到轴定义的平面中的任意偏移向量, 这可以通过获取轨道轴与另一个双随机向量的叉积来得到. 这样计算得到的是投射在轨道平面上的随机向量, 我们需要将它归一化为单位长度 :

  1. Vector3 orbitAxis = Random.onUnitSphere;
  2. cosOffset = Vector3.Cross(orbitAxis, Random.onUnitSphere).normalized;

什么是叉积(cross product)?

两个向量之间的叉积是一个几何概念, 它的定义是AB=|A||B|sin(θ)*N, 其中的N代表的是垂直于AB平面的单位向量, 因此N就是我们想要得到的法线.

叉积有一些类似点积(dot product), 但是它乘上了AB向量夹角θ的正弦值, 而不是余弦值. 如果两个向量都是单位长度, 并且θ是90度, 那么叉积的结果就是1. 不过多数时候并不是这种情况, 我们必须将叉积的结果进行归一化. 只要θ的角度不是0度或180度时这个规则适用, 因为0和180度时的正弦值是0.

在代数中, 对于一个3维向量, 叉积的定义是 A*B = 翻译中 : 形造卫星 - 图6

float crossProduct = v1.yzx v2.zxy - v1.zxy v2.yzx;(这里没看懂)

在视觉上, 叉积的向量大小就等于进行叉积计算的两个向量构成的平行四边形的面积.
翻译中 : 形造卫星 - 图7
叉积图示

需要注意的是, 叉积是一个向量, 是有方向的, 所以如果AB并不等于BA, 而是等于-B*A

上述代码多数时候有效, 但是万一第二个随机向量的方向等于负的orbitAxis, 就会得到0这个结果, 而使得归一化后的向量也是0. 我们可以使用一些额外的代码监测这一情况, 并进行处理. 具体的方式就是检查叉积结果的向量平方是否灯光与0来判断, 不过因为计算机数值精度的原因, 我们用小于0.1这一条件来代替等于0这一条件, 依然可以正确的判断出叉积的结果是否为0, 因为叉积结果归一化后, 如果不是1, 它就是0 :

  1. do {
  2. cosOffset = Vector3.Cross(orbitAxis, Random.onUnitSphere).normalized;
  3. }
  4. while (cosOffset.sqrMagnitude < 0.1f);//归一化后的向量平方小于0.1, 说明叉积结果是0, 就重新随机一个轴来计算叉积

**do** **while** 循环代码是如何工作的?

do后面括号中的代码会先被执行一次, 然后判断while圆括号中的条件是否满足, 如果满足, 则重新执行do括号中的代码, 否则结束do while循环.
**do** {
Work();
}
**while** (Condition());

等价于

Work();
**while** (Condition()) {
Work();
}

第二个偏移值可以通过计算cosOffset与orbitAxis的叉积得到, 然后通过轨道半径来缩放偏移距离 :

  1. //sinOffset = Vector3.forward;
  2. sinOffset = Vector3.Cross(cosOffset, orbitAxis);
  3. cosOffset *= radius;
  4. sinOffset *= radius;

SomberGeneralLacewing-mobile.mp4 (391.65KB)随机轨道

潮汐锁定

虽然卫星现在环绕母星进行圆周运动, 但是它们自身并不进行旋转, 现在我们要对它们进行潮汐锁定, 也就是让它们在环绕母星运动的同时, 始终保持某个方向面对母星.

为了做到这一点, 就需要卫星本身按照与轨道运动一样的频率进行自身的旋转, 并且方向与轨道圆周运动相反, 我们可以用轨道运动的频率乘以负的360度来得到卫星自身旋转所需要的的角速度 :

  1. cosOffset *= radius;
  2. sinOffset *= radius;
  3. shape.AddBehavior<RotationShapeBehavior>().AngularVelocity = -360f * frequency * orbitAxis;

不过此时我们对卫星施加的角速度, 其旋转方向是相对于世界坐标系的, 只有我们将该角速度变幻为相对于轨道轴的坐标系时才能实现卫星始终朝向母星的效果, 对卫星的transform调用InverseTransformDirection 方法就可以实现这一目的 :

  1. shape.AddBehavior<RotationShapeBehavior>().AngularVelocity = -360f * frequency * shape.transform.InverseTransformDirection(orbitAxis);

SomberGeneralLacewing-mobile.mp4 (224.52KB)潮汐锁定效果

Shape References

Satellites function correctly as long as their focus shape exists, but things get weird when the focus is recycled while the satellite is still around. Initially, the satellite will keep orbiting the last position of its focus. When the focus shape gets reused for a new spawn, the satellite still orbits it, teleporting to its new position.
We have to sever the connection between a satellite and its focus when the focus is recycled. If we destroyed the focus, then all we needed to do was check whether the focusShape reference has become null. But we recycle shapes, so the reference remains intact even though the shape isn’t part of the game anymore. So we have to find a way to determine whether a shape reference is still valid.

Instance Identification

We can distinguish between different incarnations of the same shape by adding an instance identifier property to **Shape**. Just like Age, it has to be publicly accessible but will only be modified by the shape itself.
public float Age { get; private set; }

public int InstanceId { get; private set; }
Each time a shape is recycled, increment its instance identifier. That way we can tell whether we’re dealing with the same or a recycled shape instance.
public void Recycle () {
Age = 0f;
InstanceId += 1;

}
By keeping track of both a reference to the shape and the correct instance identifier, we’re able to check whether the shape’s identifier is still the same each update. If not, it got recycled and is no longer valid.

Indirect References

Rather than explicitly add an identifier field each time we need a **Shape** reference, lets combine both in a new **ShapeInstance** struct. We’ll make this a serializable struct with a **Shape** and an instance identifier field. The shape has to be publicly accessible, but the instance identified is a technicality that doesn’t have to be public.
[System.Serializable]
public struct ShapeInstance {

public Shape Shape { get; private set; }

int instanceId;
}
The idea is that a **ShapeInstance** struct is immutable, representing a reference to a specific shape instance that’s only valid until that shape is recycled. The only way to create a valid instance reference is via a constructor method that has a single shape parameter, which we use to set the reference and copy its current instance identifier.
public ShapeInstance (Shape shape) {
Shape = shape;
instanceId = shape.InstanceId;
}
To verify whether the instance reference is valid, add an IsValid getter property that checks whether the shape’s instance identifier is still the same.
public bool IsValid {
get {
return instanceId == Shape.InstanceId;
}
}
But there is still a default constructor, which is used for example when a **ShapeInstance** array is created. That would result in null references, so we should also check whether we have a shape reference at all. That also guarantees that instances become invalid if for some reason a shape object is destroyed instead of recycled.
return Shape && instanceId == Shape.InstanceId;

Casting from Shape to Instance

Converting a **Shape** shape reference to a **ShapeInstance** value can now be done by via **new** **ShapeInstance**(shape). But we can made the code even shorter by adding a casting operator to **ShapeInstance**. An operator is defined like a method, except that it is static, includes the **operator** keyword, and doesn’t have a method name. In the case of an explicit cast, we have to add the **explicit** keyword in front of **operator**.
public static explicit operator ShapeInstance (Shape shape) {
return new ShapeInstance(shape);
}
Now the conversion can be done via (**ShapeInstance**)shape. But it can become even shorter, by making the cast implicit instead of explicit. Then a direct assignment of shape to a **ShapeInstance** field or variable is enough. That’s also how Unity supports implicit conversions between [Vector2](http://docs.unity3d.com/Documentation/ScriptReference/Vector2.html) and [Vector3](http://docs.unity3d.com/Documentation/ScriptReference/Vector3.html) and other struct types.
public static implicit operator ShapeInstance (Shape shape) {
return new ShapeInstance(shape);
}

Focal Shape Instance

Change the focalShape reference in **SatelliteShapeBehavior** into a **ShapeInstance** value. Because of the implicit cast, we don’t have to change the code in Initialize.
ShapeInstance focalShape;
We do have to change GameUpdate, because we now have to indirectly access the focal shape via focalShape.Shape. Also, we must only do this if the focal shape is still valid.
public override void GameUpdate (Shape shape) {
if (focalShape.IsValid) {
float t = 2f Mathf.PI frequency shape.Age;
shape.transform.localPosition =
focalShape.Shape.transform.localPosition +
cosOffset
Mathf.Cos(t) + sinOffset * Mathf.Sin(t);
}
}

Free Satellites

From now on, satellites orbit their focus as long as it is still in the game and stop moving when the focus is recycled. At that point the link between them has become invalid and is no longer used to update the satellite. But the **SatelliteShapeBehavior** is still attached to the satellite shape. Its GameUpdate method still gets invoked each update, even through that is now pointless. Ideally, the behavior is recycled too.

Removing Behavior

It is possible for satellite behavior to become useless, and we could create many other kinds of temporary behavior. So let’s make it possible for shapes to rid themselves of behavior that is no longer useful. We’ll do that by having the behavior tell their shape whether they’re still needed. We’ll have GameUpdate return a boolean to indicate this, so adjust the method definition in **ShapeBehavior**.
public abstract bool GameUpdate (Shape shape);
Adjust the GameUpdate overrides in all shape behaviors too, always returning **true** at the end.
public override bool GameUpdate (Shape shape) {

return true;
}
Except for **SatelliteShapeBehavior**, which should return **true** only when the focus shape is valid. Otherwise, it returns **false**, indicating that it is no longer useful and can be removed.
public override bool GameUpdate (Shape shape) {
if (focalShape.IsValid) {

return true;
}

return false;
}
In **Shape**.GameUpdate, we must now check each iteration whether the behavior is still needed. If not, recycle it, remove it from the behavior list, and then decrement the iterator so we won’t skip any behavior. We can simply invoke RemoveAt on the list, so the order of behavior isn’t changed. The behavior list should be short, so we don’t need to worry about optimizing the removal by shuffling the order like we do when deleting from the shape list.
public void GameUpdate () {
Age += Time.deltaTime;
for (int i = 0; i < behaviorList.Count; i++) {
if (!behaviorList[i].GameUpdate(this)) {
behaviorList[i].Recycle();
behaviorList.RemoveAt(i—);
}
}
}

Conservation of Momentum

Satellites now become regular shapes when their focus shape ceases to exist. Without their satellite behavior, they no longer move, but they keep their rotation because that’s a separate behavior. But it is both more interesting and more realistic if the shapes keep moving in whatever direction they were going when the focus shape disappeared. It would be as if the satellites got ejected from their system.
To make continued motion possible, we have to know the satellite’s velocity at all times, which depends on both its orbital motion and the movement of its focus. Rather than figure that out, we’ll simply keep track of the satellite’s position before its last update. We can use that to determine the last position delta and convert that to a velocity when we need it.
Add a previousPosition vector field to **SatelliteShapeBehavior**, copy the current position to it before calculating the new position, and add a movement behavior to the shape when the satellite behavior is no longer needed.
Vector3 previousPosition;



public override bool GameUpdate (Shape shape) {
if (focalShape.IsValid) {
float t = 2f Mathf.PI frequency shape.Age;
previousPosition = shape.transform.localPosition;
shape.transform.localPosition =
focalShape.Shape.transform.localPosition +
cosOffset
Mathf.Cos(t) + sinOffset Mathf.Sin(t);
return *true
;
}

  1. shape.AddBehavior<**MovementShapeBehavior**>().Velocity =<br /> (shape.transform.localPosition - previousPosition);<br /> **return** **false**;<br /> }<br />To arrive at a correct velocity, we have to divide the position delta by the time delta of the previous frame. We'll simply assume that the delta is the same as for the current frame, which is true because we're using a fixed time step.<br /> shape.AddBehavior<**MovementShapeBehavior**>().Velocity =<br /> (shape.transform.localPosition - previousPosition) / [Time](http://social.msdn.microsoft.com/search/en-us?query=Time).deltaTime;<br />This works, except when the focal shape ends up invalid before the first game update of the satellite, which is unlikely but possible. In that case, the previous position vector is arbitrary, either zero for a new behavior or still containing the value of a recycled satellite behavior. At this point the satellite hasn't moved yet, so initially set the previous position to its current position, at the end of `Initialize`.<br />**public** **void** Initialize (<br /> **Shape** shape, **Shape** focalShape, **float** radius, **float** frequency<br /> ) {<br /> …
  2. GameUpdate(shape);<br /> previousPosition = shape.transform.localPosition;<br /> }<br />Escaping satellite.

Saving and Loading

Satellites are now fully functional, can deal with recycled focus shapes, and can even survive recompilation. However, we haven’t supported saving and loading them yet.
We know what that needs to be stored to persist satellite behavior. The frequency, both offset vectors, and the previous position are straightforward. We can save and load them as usual.
public override void Save (GameDataWriter writer) {
writer.Write(frequency);
writer.Write(cosOffset);
writer.Write(sinOffset);
writer.Write(previousPosition);
}

public override void Load (GameDataReader reader) {
frequency = reader.ReadFloat();
cosOffset = reader.ReadVector3();
sinOffset = reader.ReadVector3();
previousPosition = reader.ReadVector3();
}
But saving the focus shape instance requires more work. We somehow have to persist a relationship between shapes.

Shape Index

Because all shapes that are currently in the game are stored in the game’s shape list, we can use the indices of this list to uniquely identify shapes. So we can suffice with writing the shape’s index when saving the shape instance. That means that we have to know the shape’s index when saving the focus shape, so let’s add add a SaveIndex property to **Shape** for that.
public int SaveIndex { get; set; }
This property is set in **Game**.AddShape and is only useful when saving shape references.
public void AddShape (Shape shape) {
shape.SaveIndex = shapes.Count;
shapes.Add(shape);
}
We also have to make sure that the index remains correct when we shuffle the order of the shapes in DestroyShape.
void DestroyShape () {
if (shapes.Count > 0) {
int index = Random.Range(0, shapes.Count);
shapes[index].Recycle();
int lastIndex = shapes.Count - 1;
shapes[lastIndex].SaveIndex = index;
shapes[index] = shapes[lastIndex];
shapes.RemoveAt(lastIndex);
}
}

Saving a Shape Instance

Because shape instances represent a low-level fundamental part of our game and because we want to keep them as easy to work with as possible, we’ll add support for directly saving them to **GameDataWriter**. It only has to write the save index of the shape.
public void Write (ShapeInstance value) {
writer.Write(value.Shape.SaveIndex);
}
Now we can write the focal shape just like the other state in **SatelliteShapeBehavior**.Save.
public override void Save (GameDataWriter writer) {
writer.Write(focalShape);

}

Loading a Shape Instance

When loading a shape instance, we will end up reading a save index. We need to be able to convert that to an actual shape reference. Add a public GetShape method to **Game** for that, with an index parameter. It simply returns a reference to the corresponding shape.
public Shape GetShape (int index) {
return shapes[index];
}
To convert directly from a save index to a shape instance, let’s add an alternative constructor method to **ShapeInstance** that has an index parameter instead of a **Shape** parameter. It can use the new GetShape method to retrieve the shape and then set its instance identifier.
public ShapeInstance (int saveIndex) {
Shape = Game.Instance.GetShape(saveIndex);
instanceId = Shape.InstanceId;
}
Add a ReadShapeInstance method to **GameDataReader** that reads an integer an uses it to construct a new shape instance.
public ShapeInstance ReadShapeInstance () {
return new ShapeInstance(reader.ReadInt32());
}
That allows us to read the shape instance in **SatelliteShapeBehavior**.Load.
public override void Load (GameDataReader reader) {
focalShape = reader.ReadShapeInstance();

}

Resolving Shape Instances

Saving a loading satellite data now works, but only if no shapes have been removed during the game before saving. If shapes have been destroyed, the order of the shape list changed and it is possible that satellite shapes end up with a lower index than their focus shape. If a satellite is loaded before its focus shape, it makes no sense to immediately retrieve a reference to its focus. We have to postpone retrieving the shapes until after all shapes have been loaded.
We can still load the shape instances, but delay resolving the shape references until later. This requires us to temporarily store the save index in the shape instance. Rather than using a separate field for that and increase the size of **ShapeInstance**, we can have the instance identifier field perform double duty as a save index too. Rename the field accordingly.
int instanceIdOrSaveIndex;
The constructor with a save index parameter will now store the index and set the shape reference to null instead of immediately resolving it.
public ShapeInstance (int saveIndex) {
Shape = null;
instanceIdOrSaveIndex = saveIndex;
}
Resolving the shape reference becomes an explicit separate step, for which we’ll add a public Resolve method. This approach breaks the immutability principle of the struct, but we’ll only use it once, after loading a game.
public void Resolve () {
Shape = Game.Instance.GetShape(instanceIdOrSaveIndex);
instanceIdOrSaveIndex = Shape.InstanceId;
}
Next, we need a way to signal behavior that it is time to resolve any shape instances that they might have. Add a ResolveShapeInstances method to **ShapeBehavior** for that purpose. Because only one behavior so far has need for this, we’ll provide a default empty implementation of the method, by marking it as **virtual** instead of **abstract** and giving it an empty code block.
public virtual void ResolveShapeInstances () {}
Only **SatelliteShapeBehavior** needs to override this method, in which it invokes Resolve on its focal shape instance.
public override void ResolveShapeInstances () {
focalShape.Resolve();
}
We also have to add a ResolveShapeInstances method to **Shape**, which forwards the request to all its behavior.
public void ResolveShapeInstances () {
for (int i = 0; i < behaviorList.Count; i++) {
behaviorList[i].ResolveShapeInstances();
}
}
Finally, at the end of **Game**.LoadGame, we’ll resolve the shape instances of all shapes.
IEnumerator LoadGame (GameDataReader reader) {

  1. **for** (**int** i = 0; i < shapes.Count; i++) {<br /> shapes[i].ResolveShapeInstances();<br /> }<br /> }

Dealing with Invalid Instances

Up to this point we have assumed that all shape instances are valid at the moment that the game is saved, but this is not guaranteed. We have to be able to cope with the saving and loading of invalid instances. We can indicate an invalid shape instance by writing −1.
public void Write (ShapeInstance value) {
writer.Write(value.IsValid ? value.Shape.SaveIndex : -1);
}
Reading a shape instance doesn’t require extra attention, but **ShapeInstance**.Resolve can only do its job when it has a valid save index. If not, its shape reference has to remain null and thus invalid.
public void Resolve () {
if (instanceIdOrSaveIndex >= 0) {
Shape = Game.Instance.GetShape(instanceIdOrSaveIndex);
instanceIdOrSaveIndex = Shape.InstanceId;
}
}

Shape Population Explosion

A side effect of spawning satellites along with regular shapes is that we have increased the rate at which new shapes are spawn. Currently each shape gets a satellite, thus to keep the amount of shapes stable the destruction speed has to be set to double the creation speed.

Multiple Satellites Per Shape

We don’t have to limit ourselves to exactly one satellite per regular shape. Let’s make it configurable by adding a range for the amount of satellites per shape. We need an **IntRange** struct value for that, which we can create by duplicating **FloatRange** and changing the types used from **float** to **int**. Also, to keep the random range inclusive on both ends, we have to add one to the maximum when invoking the integer variant of [Random](http://docs.unity3d.com/Documentation/ScriptReference/Random.html).Range.
using UnityEngine;

[System.Serializable]
public struct IntRange {

public int min, max;

public int RandomValueInRange {
get {
return Random.Range(min, max + 1);
}
}
}
We can also duplicate **FloatRangeDrawer** to create a variant for the new integer range, but we don’t need to do that. The code in **FloatRangeDrawer** doesn’t care about the type of the minimum and maximum values, only that they exist. So we can use the same drawer for both **FloatRange** and **IntRange**. All we have to do is add a second [CustomPropertyDrawer](http://docs.unity3d.com/Documentation/ScriptReference/CustomPropertyDrawer.html) attribute to it. Let’s also rename the drawer to **FloatOrIntRangeDrawer**, renaming its asset file too.
[CustomPropertyDrawer(typeof(FloatRange)), CustomPropertyDrawer(typeof(IntRange))]
public class FloatOrIntRangeDrawer : PropertyDrawer { … }

Can we do the same for **FloatRangeSliderDrawer**?

Add an integer range option to **SatelliteConfiguration** to configure the amount of satellites spawned per shape.
public struct SatelliteConfiguration {

public IntRange amount;


}
In SpawnShapes, determine a random count and invoke CreateSatelliteFor that many times.
int factoryIndex = Random.Range(0, spawnConfig.factories.Length);

  1. **int** satelliteCount = spawnConfig.satellite.amount.RandomValueInRange;<br /> **for** (**int** i = 0; i < satelliteCount; i++) {<br /> CreateSatelliteFor(shape);<br /> }<br /> }<br />![](https://cdn.nlark.com/yuque/0/2020/png/631907/1583052380462-409834aa-307d-424c-89f3-26c729346aac.png#align=left&display=inline&height=90&margin=%5Bobject%20Object%5D&originHeight=180&originWidth=640&size=0&status=done&style=none&width=320)<br />Between zero and three satellites per shape.

Could we create satellites for other satellites?

Population Limit

With the amount of satellites per shape no longer constant, we cannot rely on a fixed creation and destruction speed to keep the amount of shapes constant. The destruction speed is still useful, but if we want to limit the amount of shapes then we have no choice but to add a hard limit. Let’s define a shape population limit and make it configurable per level, so add a field for it to **GameLevel**.
[SerializeField]
int populationLimit;
翻译中 : 形造卫星 - 图10Population limited to 100.
Make the limit available via a public getter property, so **Game** can access it.
public int PopulationLimit {
get {
return populationLimit;
}
}
To enforce the limit, destroy shapes at the end of **Game**.[FixedUpdate](http://docs.unity3d.com/Documentation/ScriptReference/MonoBehaviour.FixedUpdate.html) as long as there are too many of them. We’ll only do that if the limit is positive, so zero or a negative value indicates that there is no limit.
void FixedUpdate () {


int limit = GameLevel.Current.PopulationLimit;
if (limit > 0) {
while (shapes.Count > limit) {
DestroyShape();
}
}
}
Enforced population limit.
The next tutorial is Lifecycle.
repositoryPDF