Skip to content

Commit 45e032d

Browse files
authored
Merge pull request #44 from tobeyStraitjacket/dev
SnapBuilder 1.4
2 parents 9d7d3cb + 6aa1cd5 commit 45e032d

21 files changed

+993
-486
lines changed

.gitmodules

+3
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,6 @@
1010
[submodule "Straitjacket.ExtensionMethods.UnityEngine"]
1111
path = Straitjacket.ExtensionMethods.UnityEngine
1212
url = https://github.com/tobeyStraitjacket/Straitjacket.ExtensionMethods.UnityEngine
13+
[submodule "Straitjacket.Math"]
14+
path = Straitjacket.Math
15+
url = https://github.com/tobeyStraitjacket/Straitjacket.Math

BepInEx.Logger

SnapBuilder.sln

+4
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,20 @@ Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "SMLHelper.Language", "SMLHe
1313
EndProject
1414
Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Straitjacket.ExtensionMethods.UnityEngine", "Straitjacket.ExtensionMethods.UnityEngine\Straitjacket.ExtensionMethods.UnityEngine\Straitjacket.ExtensionMethods.UnityEngine.shproj", "{91E30B75-0933-43CC-98FF-7C9DCCA7F849}"
1515
EndProject
16+
Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Straitjacket.Math", "Straitjacket.Math\Straitjacket.Math\Straitjacket.Math.shproj", "{EDF9AFB9-5A40-4818-8004-95E22D5B7BA0}"
17+
EndProject
1618
Global
1719
GlobalSection(SharedMSBuildProjectFiles) = preSolution
1820
Toggle\Toggle\Toggle.projitems*{2e9fec3f-6690-46e8-b676-9778d4b1292a}*SharedItemsImports = 13
1921
BepInEx.Logger\Logger\Logger.projitems*{32f6ed8c-0f9a-409d-a404-ee068789c72f}*SharedItemsImports = 13
2022
BepInEx.Logger\Logger\Logger.projitems*{90b8cfbb-759d-4e62-b923-05c2fefe5cb3}*SharedItemsImports = 4
2123
SMLHelper.Language\SMLHelper.Language\Language.projitems*{90b8cfbb-759d-4e62-b923-05c2fefe5cb3}*SharedItemsImports = 4
2224
Straitjacket.ExtensionMethods.UnityEngine\Straitjacket.ExtensionMethods.UnityEngine\Straitjacket.ExtensionMethods.UnityEngine.projitems*{90b8cfbb-759d-4e62-b923-05c2fefe5cb3}*SharedItemsImports = 4
25+
Straitjacket.Math\Straitjacket.Math\Straitjacket.Math.projitems*{90b8cfbb-759d-4e62-b923-05c2fefe5cb3}*SharedItemsImports = 4
2326
Toggle\Toggle\Toggle.projitems*{90b8cfbb-759d-4e62-b923-05c2fefe5cb3}*SharedItemsImports = 4
2427
Straitjacket.ExtensionMethods.UnityEngine\Straitjacket.ExtensionMethods.UnityEngine\Straitjacket.ExtensionMethods.UnityEngine.projitems*{91e30b75-0933-43cc-98ff-7c9dcca7f849}*SharedItemsImports = 13
2528
SMLHelper.Language\SMLHelper.Language\Language.projitems*{ad133c9e-a9a1-4dbc-bd93-7149d32cb98a}*SharedItemsImports = 13
29+
Straitjacket.Math\Straitjacket.Math\Straitjacket.Math.projitems*{edf9afb9-5a40-4818-8004-95e22d5b7ba0}*SharedItemsImports = 13
2630
EndGlobalSection
2731
GlobalSection(SolutionConfigurationPlatforms) = preSolution
2832
BELOWZERO|Any CPU = BELOWZERO|Any CPU

SnapBuilder/AimTransform.cs

+276
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
using Straitjacket.ExtensionMethods.UnityEngine;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using UnityEngine;
5+
6+
namespace Straitjacket.Subnautica.Mods.SnapBuilder
7+
{
8+
using ExtensionMethods;
9+
10+
internal class AimTransform : MonoBehaviour
11+
{
12+
private static AimTransform main;
13+
public static AimTransform Main => main == null
14+
? new GameObject("SnapBuilder").AddComponent<AimTransform>()
15+
: main;
16+
17+
public static bool Raycast(Vector3 from, Vector3 direction, out RaycastHit hit) =>
18+
Physics.Raycast(from, direction, out hit, Builder.placeMaxDistance,
19+
Builder.placeLayerMask, QueryTriggerInteraction.Ignore);
20+
21+
/// <summary>
22+
/// The camera transform, as per the original Builder.GetAimTransform()
23+
/// </summary>
24+
public Transform BuilderAimTransform => MainCamera.camera.transform;
25+
26+
private Transform offsetAimTransform;
27+
/// <summary>
28+
/// A non-moving parent of the MainCamera transform, to counteract head-bobbing
29+
/// </summary>
30+
public Transform OffsetAimTransform => offsetAimTransform ??=
31+
BuilderAimTransform.FindAncestor("camOffset").parent
32+
?? BuilderAimTransform.FindAncestor(transform => !transform.position.Equals(BuilderAimTransform.position))
33+
?? BuilderAimTransform;
34+
35+
private int lastCalculationFrame;
36+
37+
private Transform GetOrientedTransform(Vector3? position = null, Vector3? forward = null)
38+
{
39+
position ??= OffsetAimTransform.position;
40+
transform.position = position.Value;
41+
42+
forward ??= BuilderAimTransform.forward;
43+
transform.forward = forward.Value;
44+
45+
return transform;
46+
}
47+
48+
/// <summary>
49+
/// A replacement for <see cref="Builder.GetAimTransform"/> that performs all
50+
/// appropriate snapping calculations ahead of returning the modified transform.
51+
/// </summary>
52+
/// <returns></returns>
53+
public Transform GetAimTransform()
54+
{
55+
if (!SnapBuilder.Config.Snapping.Enabled)
56+
{
57+
return BuilderAimTransform;
58+
}
59+
60+
// Skip recalculating multiple times per frame
61+
if (lastCalculationFrame == Time.frameCount)
62+
{
63+
return transform;
64+
}
65+
lastCalculationFrame = Time.frameCount;
66+
67+
// If no hit, exit early
68+
if (!Raycast(OffsetAimTransform.position,
69+
BuilderAimTransform.forward,
70+
out RaycastHit hit))
71+
{
72+
return GetOrientedTransform();
73+
}
74+
75+
RaycastHit improvedColliderHit = GetImprovedColliderHit(hit);
76+
if (improvedColliderHit.collider is null)
77+
{
78+
return GetOrientedTransform();
79+
}
80+
81+
Transform hitTransform = improvedColliderHit.GetOptimalTransform();
82+
RaycastHit localisedHit = GetLocalisedHit(improvedColliderHit, hitTransform);
83+
RaycastHit snappedHit = GetSnappedHit(localisedHit);
84+
RaycastHit worldSpaceHit = GetWorldSpaceHit(snappedHit, hitTransform);
85+
RaycastHit poppedHit = PopHitOntoBestSurface(worldSpaceHit);
86+
87+
return GetOrientedTransform(forward: poppedHit.point - transform.position);
88+
}
89+
90+
/// <summary>
91+
/// Where applicable, gets a new hit after improving/reverting the collider
92+
/// </summary>
93+
/// <param name="hit"></param>
94+
/// <returns></returns>
95+
private RaycastHit GetImprovedColliderHit(RaycastHit hit)
96+
{
97+
if (ColliderCache.Main.GetRecord(hit.collider) is ColliderRecord record && record.IsImprovable)
98+
{
99+
if (SnapBuilder.Config.DetailedCollider.Enabled)
100+
{
101+
record.Improve();
102+
if (record.IsImproved)
103+
{
104+
Raycast(OffsetAimTransform.position, BuilderAimTransform.forward, out hit);
105+
}
106+
}
107+
else if (!SnapBuilder.Config.DetailedCollider.Enabled)
108+
{
109+
record.Revert();
110+
if (!record.IsImproved)
111+
{
112+
Raycast(OffsetAimTransform.position, BuilderAimTransform.forward, out hit);
113+
}
114+
}
115+
116+
if (hit.collider is Collider
117+
&& SnapBuilder.Config.RenderImprovableColliders)
118+
{
119+
record.Render();
120+
}
121+
}
122+
123+
return hit;
124+
}
125+
126+
/// <summary>
127+
/// Get a new hit where the point and normal are localised the given transform
128+
/// </summary>
129+
/// <param name="hit"></param>
130+
/// <param name="transform"></param>
131+
/// <returns></returns>
132+
private RaycastHit GetLocalisedHit(RaycastHit hit, Transform transform = null)
133+
{
134+
transform ??= hit.transform;
135+
hit.point = transform.InverseTransformPoint(hit.point); // Get the hit point localised relative to the hit transform
136+
hit.normal = transform.InverseTransformDirection(hit.normal).normalized; // Get the hit normal localised to the hit transform
137+
return hit;
138+
}
139+
140+
/// <summary>
141+
/// Gets a new hit where the point is snapped based on the normal and current round factor
142+
/// </summary>
143+
/// <param name="hitPoint"></param>
144+
/// <param name="hitNormal"></param>
145+
/// <returns></returns>
146+
private static RaycastHit GetSnappedHit(RaycastHit hit)
147+
{
148+
Vector3 hitPoint = hit.point;
149+
Vector3 hitNormal = hit.normal;
150+
151+
hitNormal.x = Mathf.Abs(hitNormal.x);
152+
hitNormal.y = Mathf.Abs(hitNormal.y);
153+
hitNormal.z = Mathf.Abs(hitNormal.z);
154+
hitNormal = hitNormal.normalized; // For sanity's sake, make sure the normal is normalised
155+
156+
// Get the rounding factor from user options based on whether the fine snapping key is held or not
157+
float roundFactor = SnapBuilder.Config.FineSnapping.Enabled ? SnapBuilder.Config.FineSnapRounding / 2f : SnapBuilder.Config.SnapRounding;
158+
159+
// Round (snap) the localised hit point coords only on axes where the corresponding normal axis is less than 1
160+
if (hitNormal.x < 1)
161+
{
162+
hitPoint.x = Math.RoundToNearest(hitPoint.x, roundFactor);
163+
}
164+
if (hitNormal.y < 1)
165+
{
166+
hitPoint.y = Math.RoundToNearest(hitPoint.y, roundFactor);
167+
}
168+
if (hitNormal.z < 1)
169+
{
170+
hitPoint.z = Math.RoundToNearest(hitPoint.z, roundFactor);
171+
}
172+
173+
hit.point = hitPoint;
174+
return hit;
175+
}
176+
177+
/// <summary>
178+
/// Gets a new hit in world space
179+
/// </summary>
180+
/// <param name="hit"></param>
181+
/// <param name="transform"></param>
182+
/// <returns></returns>
183+
private static RaycastHit GetWorldSpaceHit(RaycastHit hit, Transform transform = null)
184+
{
185+
transform ??= hit.transform;
186+
hit.point = transform.TransformPoint(hit.point);
187+
hit.normal = transform.TransformDirection(hit.normal).normalized;
188+
return hit;
189+
}
190+
191+
/// <summary>
192+
/// Gets a new hit popped onto the most appropriate surface at the most appropriate point,
193+
/// or the original hit if the operation is not possible
194+
/// </summary>
195+
/// <param name="hit"></param>
196+
/// <returns></returns>
197+
private static RaycastHit PopHitOntoBestSurface(RaycastHit hit)
198+
{
199+
if (!Player.main.IsInsideWalkable())
200+
return hit;
201+
202+
switch (Builder.GetSurfaceType(hit.normal))
203+
{
204+
case SurfaceType.Wall
205+
when !Builder.allowedSurfaceTypes.Contains(SurfaceType.Wall)
206+
&& Builder.allowedSurfaceTypes.Contains(SurfaceType.Ground):
207+
208+
// Get the rotation of the object
209+
Quaternion rotation = Builder.rotationEnabled
210+
? SnapBuilder.CalculateRotation(ref Builder.additiveRotation, hit, Builder.forceUpright || Player.main.IsInsideWalkable())
211+
: Quaternion.identity;
212+
213+
if (!Builder.bounds.Any())
214+
{
215+
return hit; // if there are no bounds for some reason, just use the original hit
216+
}
217+
218+
// Get the corners of the object based on the Builder.bounds, localised to the hit point
219+
IEnumerable<Vector3> corners = Builder.bounds
220+
.Select(bounds => new { Bounds = bounds, Corners = bounds.GetCorners() })
221+
.SelectMany(x => x.Corners.Select(corner => hit.point + rotation * corner));
222+
223+
// Get the farthest corner from the player
224+
Vector3 farthestCorner = corners.OrderByDescending(x
225+
=> Vector3.Distance(x, AimTransform.Main.OffsetAimTransform.position)).First();
226+
227+
// Center the corner to the hit.point on the local X and Y axes
228+
var empty = new GameObject();
229+
var child = new GameObject();
230+
empty.transform.position = hit.point;
231+
empty.transform.forward = hit.normal;
232+
child.transform.SetParent(empty.transform);
233+
child.transform.position = farthestCorner;
234+
child.transform.localPosition = new Vector3(0, 0, child.transform.localPosition.z);
235+
Vector3 farthestCornerCentered = child.transform.position;
236+
237+
// Clean up the GameObjects as we don't need them anymore
238+
Destroy(child);
239+
Destroy(empty);
240+
241+
float offset
242+
#if SUBNAUTICA
243+
= 0.1f; // in subnautica, the collision boundary between objects is much larger than BZ
244+
#elif BELOWZERO
245+
= 0.02f;
246+
#endif
247+
248+
// Now move the hit.point outward from the wall just enough so that the object can fit
249+
Vector3 poppedPoint = hit.point + hit.normal * Vector3.Distance(farthestCornerCentered, hit.point) + hit.normal * offset;
250+
251+
// Try to get a new hit by aiming at the floor from this popped point
252+
if (Raycast(poppedPoint, Vector3.down, out RaycastHit poppedHit))
253+
{
254+
return poppedHit;
255+
}
256+
257+
break;
258+
}
259+
260+
return hit;
261+
}
262+
263+
private void Awake()
264+
{
265+
if (main != null && main != this)
266+
{
267+
Destroy(this);
268+
}
269+
else
270+
{
271+
main = this;
272+
transform.SetParent(BuilderAimTransform, false);
273+
}
274+
}
275+
}
276+
}

SnapBuilder/ColliderCache.cs

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using UnityEngine;
5+
6+
namespace Straitjacket.Subnautica.Mods.SnapBuilder
7+
{
8+
internal class ColliderCache : MonoBehaviour
9+
{
10+
private const int CleanUpSeconds = 5;
11+
12+
private static ColliderCache main;
13+
public static ColliderCache Main => main == null
14+
? new GameObject("ColliderCache").AddComponent<ColliderCache>()
15+
: main;
16+
17+
private Dictionary<Collider, ColliderRecord> records = new Dictionary<Collider, ColliderRecord>();
18+
19+
/// <summary>
20+
/// The active <see cref="ColliderRecord"/>
21+
/// </summary>
22+
public ColliderRecord Record { get; private set; }
23+
24+
/// <summary>
25+
/// Returns or initialises the <see cref="Collider"/> for a given <see cref="Collider"/>
26+
/// </summary>
27+
/// <param name="collider"></param>
28+
/// <returns></returns>
29+
public ColliderRecord GetRecord(Collider collider) => Record = records.TryGetValue(collider, out ColliderRecord record)
30+
? record
31+
: records[collider] = new ColliderRecord(collider);
32+
33+
public void RevertAll()
34+
{
35+
Record = null;
36+
foreach (var record in records.Values)
37+
{
38+
record.Revert();
39+
}
40+
}
41+
42+
private void Update()
43+
{
44+
foreach (var collider in records
45+
.Where(pair => !pair.Value.IsImproved
46+
&& DateTime.UtcNow > pair.Value.Timestamp + TimeSpan.FromSeconds(CleanUpSeconds))
47+
.Select(pair => pair.Key).ToList())
48+
{
49+
records.Remove(collider);
50+
}
51+
}
52+
53+
private void Awake()
54+
{
55+
if (main != null && main != this)
56+
{
57+
Destroy(this);
58+
}
59+
else
60+
{
61+
main = this;
62+
transform.SetParent(AimTransform.Main.transform, false);
63+
}
64+
}
65+
}
66+
}

0 commit comments

Comments
 (0)