Skip to content

Instantly share code, notes, and snippets.

@mandarinx
Created March 19, 2020 20:52
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mandarinx/57b82f2556ea492ff681206bf27cff5f to your computer and use it in GitHub Desktop.
Save mandarinx/57b82f2556ea492ff681206bf27cff5f to your computer and use it in GitHub Desktop.
Bullet system
//#define LOG_BULLETS
//#define OUTOFBOUNDS
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using TerrorSquid.Configs;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.SceneManagement;
using Debug = UnityEngine.Debug;
namespace TerrorSquid.BulletSystem {
public enum BulletPatternType {
Linear = 0,
Wave = 1,
SplitDouble = 2,
Diamond = 3,
Flower = 4,
Turret = 5,
MultiShot = 6,
Circle = 7,
Freeze = 8,
Jellyfish = 9,
Worm = 10,
}
public struct BulletInstance {
public int instanceId;
public int index;
}
public static class Bullets {
private static Transform[] tParent;
private static Transform[] tBody;
private static MeshFilter[] tBodyMesh;
private static MeshRenderer[] tBodyRenderer;
private static TrailRenderer[] trail;
private static Quaternion[] rotLocalSphere;
private static Quaternion[] rotBody;
private static Quaternion[] rotParent;
private static Vector3[] posParent;
// private static float[] grace;
private static bool[] hidden;
private static int[] instanceIds;
private static int[] explosionHash;
private static int[] despawnInstanceIds;
private static int[] patternOffset;
private static int[] patternLength;
private static int numHots;
private static int currentDespawn;
private static BulletSystemConfig bSysConfig;
private static PlanetAttributes planetAttribs;
private static int sequenceLength;
private static readonly Dictionary<int, PatternConfig> sequenceConfigs = new Dictionary<int, PatternConfig>();
private static readonly Dictionary<BulletPatternType, PatternConfig> typeConfigs = new Dictionary<BulletPatternType, PatternConfig>();
private static readonly Dictionary<int, int> instanceIdMap = new Dictionary<int, int>();
private static readonly Dictionary<BulletPatternType, int> current = new Dictionary<BulletPatternType, int>();
public static Vector3[] HotPositions { get; private set; }
public static Quaternion[] HotRotLocalSpheres { get; private set; }
public static int[] HotInstanceIds { get; private set; }
public static int NumHotBullets => numHots;
private static Action<PatternConfig, Config> VoidInit => (patternconfig, config) => {};
private static Action<PatternConfig, Quaternion, Vector2, float> VoidSpawn => (patternconfig, quaternion, vector2, dt) => {};
private static Action<int, int> VoidDespawn => (a, b) => {};
private static Action VoidDespawnAll => () => {};
private static readonly Dictionary<BulletPatternType, Action<PatternConfig, Config>> cbInit = new Dictionary<BulletPatternType, Action<PatternConfig, Config>> {
{ BulletPatternType.Linear, PatternLinear.Init },
{ BulletPatternType.Wave, PatternWave.Init },
{ BulletPatternType.SplitDouble, PatternSplitDouble.Init },
{ BulletPatternType.Diamond, VoidInit },
{ BulletPatternType.Flower, PatternFlower.Init },
{ BulletPatternType.Turret, PatternTurret.Init },
{ BulletPatternType.MultiShot, PatternMultiShot.Init },
{ BulletPatternType.Circle, PatternCircle.Init },
{ BulletPatternType.Freeze, PatternFreeze.Init },
{ BulletPatternType.Jellyfish, PatternJellyfish.Init },
{ BulletPatternType.Worm, PatternWorm.Init },
};
private static readonly Dictionary<BulletPatternType, Action<PatternConfig, Quaternion, Vector2, float>> cbSpawn = new Dictionary<BulletPatternType, Action<PatternConfig, Quaternion, Vector2, float>> {
{ BulletPatternType.Linear, PatternLinear.Spawn },
{ BulletPatternType.Wave, PatternWave.Spawn },
{ BulletPatternType.SplitDouble, PatternSplitDouble.Spawn },
{ BulletPatternType.Diamond, VoidSpawn },
{ BulletPatternType.Flower, PatternFlower.Spawn },
{ BulletPatternType.Turret, PatternTurret.Spawn },
{ BulletPatternType.MultiShot, PatternMultiShot.Spawn },
{ BulletPatternType.Circle, PatternCircle.Spawn },
{ BulletPatternType.Freeze, PatternFreeze.Spawn },
{ BulletPatternType.Jellyfish, PatternJellyfish.Spawn },
{ BulletPatternType.Worm, PatternWorm.Spawn },
};
private static readonly Dictionary<BulletPatternType, Action<int, int>> cbDespawn = new Dictionary<BulletPatternType, Action<int, int>> {
{ BulletPatternType.Linear, PatternLinear.Despawn},
{ BulletPatternType.Wave, PatternWave.Despawn },
{ BulletPatternType.SplitDouble, PatternSplitDouble.Despawn },
{ BulletPatternType.Diamond, VoidDespawn },
{ BulletPatternType.Flower, PatternFlower.Despawn },
{ BulletPatternType.Turret, PatternTurret.Despawn },
{ BulletPatternType.MultiShot, PatternMultiShot.Despawn },
{ BulletPatternType.Circle, PatternCircle.Despawn },
{ BulletPatternType.Freeze, PatternFreeze.Despawn },
{ BulletPatternType.Jellyfish, PatternJellyfish.Despawn },
{ BulletPatternType.Worm, PatternWorm.Despawn },
};
private static readonly Dictionary<BulletPatternType, Action> cbDespawnAll = new Dictionary<BulletPatternType, Action> {
{ BulletPatternType.Linear, PatternLinear.DespawnAll},
{ BulletPatternType.Wave, PatternWave.DespawnAll },
{ BulletPatternType.SplitDouble, PatternSplitDouble.DespawnAll },
{ BulletPatternType.Diamond, VoidDespawnAll },
{ BulletPatternType.Flower, PatternFlower.DespawnAll },
{ BulletPatternType.Turret, PatternTurret.DespawnAll },
{ BulletPatternType.MultiShot, PatternMultiShot.DespawnAll },
{ BulletPatternType.Circle, PatternCircle.DespawnAll },
{ BulletPatternType.Freeze, PatternFreeze.DespawnAll },
{ BulletPatternType.Jellyfish, PatternJellyfish.DespawnAll },
{ BulletPatternType.Worm, PatternWorm.DespawnAll },
};
public static void Init(BulletSystemConfig bulletsConfig, PlanetAttributes planetAttributes) {
#if UNITY_EDITOR
List<string> sequenceErrors = new List<string>();
if (!BulletPatternSequence.IsValid(bulletsConfig.Sequence, sequenceErrors)) {
foreach (string error in sequenceErrors) {
Debug.LogError(error);
}
}
#endif
bSysConfig = bulletsConfig;
planetAttribs = planetAttributes;
sequenceLength = bulletsConfig.Sequence.Count;
int totalInstances = 0;
for (int i = 0; i < sequenceLength; ++i) {
totalInstances += bulletsConfig.Sequence.Get(i).MaxNum;
}
int totalPatternTypes = Enum.GetValues(typeof(BulletPatternType)).Length;
tParent = new Transform[totalInstances];
tBody = new Transform[totalInstances];
tBodyMesh = new MeshFilter[totalInstances];
tBodyRenderer = new MeshRenderer[totalInstances];
trail = new TrailRenderer[totalInstances];
rotLocalSphere = new Quaternion[totalInstances];
rotBody = new Quaternion[totalInstances];
rotParent = new Quaternion[totalInstances];
posParent = new Vector3[totalInstances];
// grace = new float[totalInstances];
hidden = new bool[totalInstances];
instanceIds = new int[totalInstances];
explosionHash = new int[totalInstances];
despawnInstanceIds = new int[totalInstances];
HotPositions = new Vector3[totalInstances];
HotRotLocalSpheres = new Quaternion[totalInstances];
HotInstanceIds = new int[totalInstances];
patternOffset = new int[totalPatternTypes];
patternLength = new int[totalPatternTypes];
// calculate lengths
for (int i = 0; i < sequenceLength; ++i) {
PatternConfig cfg = bulletsConfig.Sequence.Get(i);
patternLength[(int)cfg.Type] += cfg.MaxNum;
}
int patternStart = 0;
for (int i = 0; i < totalPatternTypes; ++i) {
BulletPatternType type = (BulletPatternType) i;
patternOffset[(int)type] = patternStart;
patternStart += patternLength[(int)type];
current[type] = 0;
}
for (int i = 0; i < sequenceLength; ++i) {
PatternConfig cfg = bulletsConfig.Sequence.Get(i);
int nameHash = cfg.NameHash;
sequenceConfigs[nameHash] = cfg;
typeConfigs[cfg.Type] = cfg;
}
}
public static void Spawn(PatternConfig config, Quaternion rot, Vector2 dir, float dt) {
if (current[config.Type] >= config.MaxNum) {
return;
}
cbSpawn[config.Type].Invoke(config, rot, dir, dt);
}
public static BulletInstance GetInstance(int nameHash, Quaternion rotLocSphere, Vector2 dir) {
PatternConfig cfg = sequenceConfigs[nameHash];
BulletPatternType type = cfg.Type;
int cur = current[type];
if (cur >= cfg.MaxNum) {
return new BulletInstance {
instanceId = -1,
index = -1
};
}
int i = patternOffset[(int)type] + cur;
explosionHash[i] = cfg.ExplosionHash;
// grace[i] = 0f;
hidden[i] = false;
rotLocalSphere[i] = rotLocSphere;
rotParent[i] = SphereTools.GetRotation(rotLocSphere);
posParent[i] = SphereTools.GetPosition(rotLocSphere, planetAttribs.Center, planetAttribs.Radius);
rotBody[i] = SphereTools.GetRotationAroundUp(dir);
tParent[i].gameObject.SetActive(true);
current[type] += 1;
return new BulletInstance {
instanceId = instanceIds[i],
index = i
};
}
public static void Hide(int index) {
tBodyRenderer[index].enabled = false;
trail[index].enabled = false;
hidden[index] = true;
}
public static void Show(int index) {
tBodyRenderer[index].enabled = true;
trail[index].enabled = true;
hidden[index] = false;
}
public static void Decorate(int index, BulletDecorator decorator) {
trail[index].sharedMaterial = decorator.TrailMaterial;
trail[index].widthCurve = decorator.TrailWidth;
trail[index].time = decorator.TrailTime;
tBodyMesh[index].sharedMesh = decorator.BodyMesh;
tBodyRenderer[index].sharedMaterial = decorator.BodyMaterial;
}
public static Quaternion GetSpawnPos(Quaternion playerLocalSphereRotation, Vector2 direction) {
return SphereTools.GetLocalSphereRotation(playerLocalSphereRotation,
direction,
bSysConfig.DegreesSpawnOffset);
}
public static int GetNextPattern(int currentPattern, BulletPatternSequence sequence) {
int search = 1;
while (search <= sequenceLength) {
int i = (currentPattern + search) % sequenceLength;
if (sequence.Get(i).IsRitual) {
return i;
}
++search;
}
return currentPattern;
}
public static PatternConfig GetConfig(int nameHash) {
return sequenceConfigs[nameHash];
}
public static int GetExplosionHash(int instanceId) {
return explosionHash[instanceIdMap[instanceId]];
}
public static int GetInstanceIndexById(int instanceId) {
return instanceIdMap[instanceId];
}
public static Transform GetBodyByIndex(int instanceIndex) {
return tBody[instanceIndex];
}
public static void Despawn(int instanceId) {
despawnInstanceIds[currentDespawn] = instanceId;
++currentDespawn;
}
public static void DespawnDeads() {
for (int i = 0; i < currentDespawn; ++i) {
int despawnInstanceIndex = instanceIdMap[despawnInstanceIds[i]];
BulletPatternType type = GetTypeFromIndex(despawnInstanceIndex);
int cur = current[type];
int last = cur - 1;
int offset = patternOffset[(int)type];
if (despawnInstanceIndex - offset >= cur) {
continue;
}
trail[despawnInstanceIndex].Clear();
tParent[despawnInstanceIndex].gameObject.SetActive(false);
current[type] -= 1;
// #if OUTOFBOUNDS
// if (last < 0 || last >= CONST.NUM_BULLETS) {
// Throw($"DespawnDeads | FROM out of bounds | from: {last} " +
// $"instanceId: {despawnInstanceIds[i]}" +
// $"despawnIndex: {despawnInstanceIndex}" +
// $"offset: {offset}" +
// $"pattern type: {type}");
// }
// if (despawnInstanceIndex - offset < 0 || despawnInstanceIndex - offset >= CONST.NUM_BULLETS) {
// Throw($"DespawnDeads | TO out of bounds | to: {despawnInstanceIndex - offset} " +
// $"instanceId: {despawnInstanceIds[i]}" +
// $"despawnIndex: {despawnInstanceIndex}" +
// $"offset: {offset}" +
// $"pattern type: {type}");
// }
// #endif
cbDespawn[type].Invoke(last, despawnInstanceIndex - offset);
// Temporarily store the despawned data
TrailRenderer dTrail = trail[despawnInstanceIndex];
Transform dParent = tParent[despawnInstanceIndex];
Transform dBody = tBody[despawnInstanceIndex];
MeshFilter dBodyMesh = tBodyMesh[despawnInstanceIndex];
MeshRenderer dBodyRenderer = tBodyRenderer[despawnInstanceIndex];
Quaternion dRotLocalSphere = rotLocalSphere[despawnInstanceIndex];
Quaternion dRotBody = rotBody[despawnInstanceIndex];
Quaternion dRotParent = rotParent[despawnInstanceIndex];
Vector3 dPosParent = posParent[despawnInstanceIndex];
// float dGrace = grace[despawnInstanceIndex];
bool dHidden = hidden[despawnInstanceIndex];
int dInstanceId = instanceIds[despawnInstanceIndex];
int dExplHash = explosionHash[despawnInstanceIndex];
last += offset;
// Move data at the last spot to the despawned slot
trail[despawnInstanceIndex] = trail[last];
tParent[despawnInstanceIndex] = tParent[last];
tBody[despawnInstanceIndex] = tBody[last];
tBodyMesh[despawnInstanceIndex] = tBodyMesh[last];
tBodyRenderer[despawnInstanceIndex] = tBodyRenderer[last];
rotLocalSphere[despawnInstanceIndex] = rotLocalSphere[last];
rotBody[despawnInstanceIndex] = rotBody[last];
rotParent[despawnInstanceIndex] = rotParent[last];
posParent[despawnInstanceIndex] = posParent[last];
// grace[despawnInstanceIndex] = grace[last];
hidden[despawnInstanceIndex] = hidden[last];
explosionHash[despawnInstanceIndex] = explosionHash[last];
instanceIds[despawnInstanceIndex] = instanceIds[last];
instanceIdMap[instanceIds[last]] = despawnInstanceIndex;
// Assign the despawned data to the last slot
trail[last] = dTrail;
tParent[last] = dParent;
tBody[last] = dBody;
tBodyMesh[last] = dBodyMesh;
tBodyRenderer[last] = dBodyRenderer;
rotLocalSphere[last] = dRotLocalSphere;
rotBody[last] = dRotBody;
rotParent[last] = dRotParent;
posParent[last] = dPosParent;
// grace[last] = dGrace;
hidden[last] = dHidden;
explosionHash[last] = dExplHash;
instanceIds[last] = dInstanceId;
instanceIdMap[dInstanceId] = last;
// Log($"InstanceId (despawned: {dInstanceId} > {last}) (last: {lastId} => {newLastId})");
}
currentDespawn = 0;
}
private static BulletPatternType GetTypeFromIndex(int index) {
int n = 0;
for (int i = 0; i < patternOffset.Length; ++i) {
if (index >= patternOffset[i]) {
n = i;
continue;
}
break;
}
return (BulletPatternType)n;
}
public static void DespawnAll() {
for (int i = 0; i < patternOffset.Length; ++i) {
int offset = patternOffset[i];
BulletPatternType type = (BulletPatternType) i;
int len = current[type];
for (int j = offset; j < offset + len; ++j) {
trail[j].Clear();
tParent[j].gameObject.SetActive(false);
}
cbDespawnAll[type].Invoke();
current[type] = 0;
}
currentDespawn = 0;
}
public static void Update(float dt, Quaternion playerRot, Vector2 playerDirection) {
// UpdateGrace(BulletPatternType.Linear, dt);
// UpdateGrace(BulletPatternType.Wave, dt);
// UpdateGrace(BulletPatternType.SplitDouble, dt);
// UpdateGrace(BulletPatternType.Diamond, dt);
// UpdateGrace(BulletPatternType.Flower, dt);
// UpdateGrace(BulletPatternType.Turret, dt);
// UpdateGrace(BulletPatternType.MultiShot, dt);
// UpdateGrace(BulletPatternType.Circle, dt);
// UpdateGrace(BulletPatternType.Freeze, dt);
PatternLinear.Update(dt, patternOffset[(int)BulletPatternType.Linear], rotLocalSphere, rotBody, playerRot, playerDirection);
PatternWave.Update(dt, patternOffset[(int)BulletPatternType.Wave], rotLocalSphere, rotBody, playerRot, playerDirection);
PatternSplitDouble.Update(dt, patternOffset[(int)BulletPatternType.SplitDouble], rotLocalSphere, rotBody, playerRot, playerDirection);
// PatternDiamond.Update(dt, patternOffset[(int)BulletPatternType.Diamond], rotLocalSphere, rotBody, playerRot, playerDirection);
PatternFlower.Update(dt, patternOffset[(int)BulletPatternType.Flower], rotLocalSphere, rotBody, playerRot, playerDirection);
PatternTurret.Update(dt, patternOffset[(int)BulletPatternType.Turret], rotLocalSphere, rotBody, playerRot, playerDirection);
PatternMultiShot.Update(dt, patternOffset[(int)BulletPatternType.MultiShot], rotLocalSphere, rotBody, playerRot, playerDirection);
PatternCircle.Update(dt, patternOffset[(int)BulletPatternType.Circle], rotLocalSphere, rotBody, playerRot, playerDirection);
PatternFreeze.Update(dt, patternOffset[(int)BulletPatternType.Freeze], rotLocalSphere, rotBody, playerRot, playerDirection);
PatternJellyfish.Update(dt, patternOffset[(int)BulletPatternType.Jellyfish], rotLocalSphere, rotBody, playerRot, playerDirection);
PatternWorm.Update(dt, patternOffset[(int)BulletPatternType.Worm], rotLocalSphere, rotBody, playerRot, playerDirection);
WriteTransforms(BulletPatternType.Linear);
WriteTransforms(BulletPatternType.Wave);
WriteTransforms(BulletPatternType.SplitDouble);
// WriteTransforms(BulletPatternType.Diamond);
WriteTransforms(BulletPatternType.Flower);
WriteTransforms(BulletPatternType.Turret);
WriteTransforms(BulletPatternType.MultiShot);
WriteTransforms(BulletPatternType.Circle);
WriteTransforms(BulletPatternType.Freeze);
WriteTransforms(BulletPatternType.Jellyfish);
WriteTransforms(BulletPatternType.Worm);
}
public static void CacheHotBullets() {
numHots = 0;
for (int i = 0; i < patternOffset.Length; ++i) {
int offset = patternOffset[i];
BulletPatternType type = (BulletPatternType)i;
int len = current[type];
for (int j = offset; j < offset + len; ++j) {
// if (grace[j] < bSysConfig.GracePeriodAtSpawn) {
// continue;
// }
if (hidden[j]) {
continue;
}
HotPositions[numHots] = posParent[j];
HotRotLocalSpheres[numHots] = rotLocalSphere[j];
HotInstanceIds[numHots] = instanceIds[j];
++numHots;
}
}
}
// private static void UpdateGrace(BulletPatternType type, float dt) {
// int from = patternOffset[(int)type];
// int to = from + current[type];
// for (int i = from; i < to; ++i) {
// grace[i] += dt;
// }
// }
private static void WriteTransforms(BulletPatternType type) {
int from = patternOffset[(int)type];
int to = from + current[type];
for (int i = from; i < to; ++i) {
rotParent[i] = SphereTools.GetRotation(rotLocalSphere[i]);
posParent[i] = SphereTools.GetPosition(rotLocalSphere[i], planetAttribs.Center, planetAttribs.Radius);
tParent[i].rotation = rotParent[i];
tParent[i].position = posParent[i];
tBody[i].localRotation = rotBody[i];
}
}
public static IEnumerator Load(LoadState loadState, Config config) {
loadState.bulletsToLoad = tParent.Length;
loadState.bulletsLoaded = 0;
Scene sceneBullets = SceneManager.CreateScene("Bullets");
for (int i = 0; i < patternOffset.Length; ++i) {
BulletPatternType type = (BulletPatternType)i;
if (!typeConfigs.ContainsKey(type)) {
continue;
}
int offset = patternOffset[i];
PatternConfig cfg = typeConfigs[type];
string bulletName = cfg.name;
int length = patternLength[i];
int n = offset;
for (int b = 0; b < length; ++b) {
Instance instance = CreateInstance($"{bulletName}_{b:0000}");
SceneManager.MoveGameObjectToScene(instance.parent, sceneBullets);
tParent[n] = instance.parent.transform;
tBody[n] = instance.body.transform;
tBodyMesh[n] = instance.bodyMesh;
tBodyRenderer[n] = instance.bodyRenderer;
trail[n] = instance.trailRenderer;
// grace[n] = 0f;
hidden[n] = false;
instanceIds[n] = instance.instanceId;
instanceIdMap.Add(instanceIds[n], n);
++n;
++loadState.bulletsLoaded;
if (n % CONST.INSTANTIATES_PER_FRAME == 0) {
yield return null;
}
}
cbInit[type].Invoke(cfg, config);
}
}
public struct Instance {
public GameObject parent;
public GameObject body;
public TrailRenderer trailRenderer;
public MeshFilter bodyMesh;
public MeshRenderer bodyRenderer;
public int instanceId;
}
public static Instance CreateInstance(string name) {
Instance instance = new Instance();
instance.parent = new GameObject(name) {
layer = LayerHelper.PostProcessing
};
instance.instanceId = instance.parent.GetInstanceID();
instance.body = new GameObject("Body") {
layer = LayerHelper.PostProcessing
};
instance.parent.SetActive(false);
TrailRenderer tr = instance.parent.AddComponent<TrailRenderer>();
tr.shadowCastingMode = ShadowCastingMode.Off;
tr.receiveShadows = false;
tr.motionVectorGenerationMode = MotionVectorGenerationMode.ForceNoMotion;
tr.allowOcclusionWhenDynamic = false;
instance.trailRenderer = tr;
instance.bodyMesh = instance.body.AddComponent<MeshFilter>();
MeshRenderer mr = instance.body.AddComponent<MeshRenderer>();
mr.shadowCastingMode = ShadowCastingMode.Off;
mr.receiveShadows = false;
mr.lightProbeUsage = LightProbeUsage.Off;
tr.reflectionProbeUsage = ReflectionProbeUsage.Off;
tr.motionVectorGenerationMode = MotionVectorGenerationMode.ForceNoMotion;
tr.allowOcclusionWhenDynamic = false;
instance.bodyRenderer = mr;
instance.body.transform.SetParent(instance.parent.transform);
return instance;
}
[Conditional("OUTOFBOUNDS")]
private static void Throw(object msg) {
throw new IndexOutOfRangeException(msg.ToString());
}
[Conditional("LOG_BULLETS")]
private static void Log(object msg) {
FrameLogger.Log(msg.ToString());
}
}
}
using TerrorSquid.Core;
using UnityEngine;
namespace TerrorSquid.BulletSystem {
public static class PatternLinear {
private static Vector2[] direction;
private static float[] speed;
private static int current;
public static void Init(PatternConfig cfg, Config config) {
direction = new Vector2[cfg.MaxNum];
speed = new float[cfg.MaxNum];
}
public static void Spawn(PatternConfig config, Quaternion rotLocalSphere, Vector2 dir, float dt) {
BulletInstance bi = Bullets.GetInstance(config.NameHash, rotLocalSphere, dir);
Bullets.Decorate(bi.index, config.BulletDecorator);
direction[current] = dir;
speed[current] = ((PatternConfigLinear)config).Speed;
++current;
}
public static void Despawn(int from, int to) {
direction[to] = direction[from];
speed[to] = speed[from];
--current;
}
public static void DespawnAll() {
current = 0;
}
public static void Update(float dt,
int from,
Quaternion[] rotLocalSphere,
Quaternion[] rotBody,
Quaternion playerRotLocalSphere,
Vector2 playerDirection) {
for (int i = 0; i < current; ++i) {
rotLocalSphere[from + i] = SphereTools.GetLocalSphereRotation(rotLocalSphere[from + i],
direction[i],
TimeTracks.bulletMovement.DeltaTime * speed[i]);
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment