Reputation: 35
I'm hitting my head against the wall here--> Task: for my little project (isometric with tilted camera), i would like to let the player create a custom area of effect for his ability, meaning, whence he activates the skill, he can drag the mouse and create a custom area (like an indicator to point what area the skill will cover), with fixed width and max length tied to the skill's max length. The starting point should be inside its max radius (but he can extend it far). So, my idea was to use a Mesh (a game object with a mesh filter and mesh renderer). I've created a prefab outta of it. To handle the loop, i've this async Task here, that waits until condition are met:
public class FreeAOEState : SpellState
{
public override async Task ExcecuteState(SpellTemplate spellTemplate)
{
var spellRadius = spellTemplate.GetParameter<float>(SpellParameterType.SpellRange);
var maxLength = spellTemplate.GetParameter<float>(SpellParameterType.AreaSize);
var width = spellTemplate.GetTargetParameter<float>(SpellTargetFeatures.Width);
bool isDrawningStarted = false;
MeshFilter meshFilter = new();
// control points for spline
List<Vector3> controlPoints = new();
float totalLength = 0f;
spellTemplate.spellIndicator.InitializeFreeAOE(spellRadius);
while(!isDrawningStarted)
{
if (Input.GetKeyDown(KeyCode.Mouse0))
{
if(spellTemplate.spellIndicator.StartFreeAOEDrawning(spellRadius,
out Vector3 startPoint, controlPoints))
{
spellTemplate.spellIndicator.InstantiateFreeAOE(startPoint, out meshFilter);
isDrawningStarted = true;
}
}
await Task.Yield();
}
var time = 0f;
while (time < 2f * Config.MAX_TIME_READY_SPELL)
{
time += Time.deltaTime;
var result = spellTemplate.spellIndicator.UpdateFreeAEOIndicatorWithSpline(
width,
maxLength,
ref totalLength,
controlPoints,
meshFilter);
if (Input.GetKeyUp(KeyCode.Mouse0) || result.IsDrawingFinished)
{
spellTemplate.SpellEffect();
await Task.Delay(400);
break;
}
await Task.Yield();
}
spellTemplate.spellIndicator.HideCursor();
}
}
Introducing:
public struct FreeAOEIndicatorResult
{
public bool IsDrawingFinished;
public float TotalLength;
}
a simple struct as ouput spellTemplate.spellIndicator.UpdateFreeAEOIndicatorWithSpline() to check when the condition are met and the loop can be cut.
The wanted behaviour is the following: the player click the left mouse button (for now) and Instantiate the mesh prefab if the point chosen is inside the spellRadius (distance from the player). That is the condition to close first while loop. The second loop should simply add the points recorded from the mouse movement and add them to the controlPoints list to create the mesh from the spline. Breaking from it whence the left mouse has been released or length of the mesh is greater than spell's maxLength. The methods used, are placed in spellTemplate.spellIndicator script, and are the following:
First method: check if the raycast hit a point inside the radius centered on player with radius = spellRadius
public bool StartFreeAOEDrawning(float spellRadius, out Vector3 startPoint,
List<Vector3> controlPoints)
{
Ray ray = mainCamera.ScreenPointToRay(Input.mousePosition);
startPoint = Vector3.zero;
var playerPos = Player.Instance.playerTransform.position;
Debug.DrawRay(ray.origin, ray.direction * 100f, Color.green, 2f); // visual debug
if (Physics.Raycast(ray, out RaycastHit hitInfo, Mathf.Infinity))
{
if (Vector3.Distance(hitInfo.point, playerPos) > spellRadius)
{
return false;
}
startPoint = hitInfo.point;
controlPoints.Add(startPoint);
DrawHitPoint(hitInfo.point);
return true;
}
return false;
}
Create a visual rapresentation of point hit by raycasting
private void DrawHitPoint(Vector3 hitPoint)
{
// yellow sphere to check impact point
GameObject hitMarker = GameObject.CreatePrimitive(PrimitiveType.Sphere);
hitMarker.transform.position = hitPoint;
hitMarker.transform.localScale = Vector3.one * 0.3f;
hitMarker.GetComponent<Renderer>().material.color = Color.yellow;
Destroy(hitMarker, 2f);
}
Just instantiate the mesh now
public void InstantiateFreeAOE(Vector3 startPoint, out MeshFilter meshFilter)
{
_currentArea = Instantiate(meshPrefabToInstantiate, startPoint, Quaternion.identity);
_currentArea.transform.position = startPoint;
if (_currentArea.TryGetComponent(out meshFilter))
{
meshFilter.mesh = new Mesh();
_currentMeshFilter = meshFilter;
}
else
{
meshFilter = null;
}
}
Just set the maxradius canvas for visual representation of max radius
public void InitializeFreeAOE(float spellRadius)
{
spellCastRadiusCanvas.SetActive(true);
_spellCastRadiusCanvasRectTransform.sizeDelta = new Vector2(spellRadius * 2f, spellRadius * 2f);
_spellCastRadiusImageRect.sizeDelta = new Vector2(spellRadius * 2f, spellRadius * 2f);
}
Now, in the main loop, we have to catch mouse position, save the points and send them to the spline creator, and create a mesh with Width and return if length > maxLength
public FreeAOEIndicatorResult UpdateFreeAEOIndicatorWithSpline(
float width,
float maxLength,
ref float totalLength,
List<Vector3> controlPoints,
MeshFilter meshFilter)
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
if (Physics.Raycast(ray, out RaycastHit hit, Mathf.Infinity))
{
Vector3 currentPoint = hit.point;
if (controlPoints.Count > 0)
{
float distance = Vector3.Distance(controlPoints[^1], currentPoint);
if (distance > 0.5f)
{
controlPoints.Add(currentPoint);
}
}
//here, we're generating the curve
var curvePoints = GenerateCatmullRomCurve(controlPoints, 20);
// here we draw the curve
DrawMeshFromCurve(curvePoints, width, meshFilter);
// retrieve the length
float length = 0f;
for (int i = 1; i < curvePoints.Count; i++)
{
length += Vector3.Distance(curvePoints[i - 1], curvePoints[i]);
}
totalLength = length;
// our condition to cut the loop
if (totalLength >= maxLength)
{
return new FreeAOEIndicatorResult { IsDrawingFinished = true, TotalLength = totalLength };
}
return new FreeAOEIndicatorResult { IsDrawingFinished = false, TotalLength = totalLength };
}
return new FreeAOEIndicatorResult { IsDrawingFinished = true, TotalLength = totalLength };
}
Now are following the methods to create the curve and to create the mesh around it
public List<Vector3> GenerateCatmullRomCurve(List<Vector3> controlPoints, int resolution)
{
List<Vector3> curvePoints = new ();
for (int i = 0; i < controlPoints.Count - 3; i++)
{
Vector3 p0 = controlPoints[i];
Vector3 p1 = controlPoints[i + 1];
Vector3 p2 = controlPoints[i + 2];
Vector3 p3 = controlPoints[i + 3];
for (int j = 0; j < resolution; j++)
{
float t = j / (float)resolution;
curvePoints.Add(CatmullRom(p0, p1, p2, p3, t));
}
}
return curvePoints;
}
private Vector3 CatmullRom(Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float t)
{
float t2 = t * t;
float t3 = t2 * t;
return 0.5f * ((2f * p1) +
(-p0 + p2) * t +
(2f * p0 - 5f * p1 + 4f * p2 - p3) * t2 +
(-p0 + 3f * p1 - 3f * p2 + p3) * t3);
}
void DrawMeshFromCurve(List<Vector3> curvePoints, float width, MeshFilter meshFilter)
{
if (curvePoints.Count < 2)
return;
int maxPoints = 1000;
if (curvePoints.Count > maxPoints)
{
curvePoints = curvePoints.GetRange(0, maxPoints);
}
int numSegments = curvePoints.Count - 1;
int[] triangles = new int[numSegments * 6];
Vector3[] vertices = new Vector3[curvePoints.Count * 2];
for (int i = 0; i < curvePoints.Count; i++)
{
Vector3 direction = (i < curvePoints.Count - 1) ?
(curvePoints[i + 1] - curvePoints[i]).normalized :
(curvePoints[i] - curvePoints[i - 1]).normalized;
Vector3 offset = Vector3.Cross(direction, Vector3.up) * (width / 2);
vertices[i * 2] = curvePoints[i] - offset;
vertices[i * 2 + 1] = curvePoints[i] + offset;
if (i < curvePoints.Count - 1)
{
int baseIndex = i * 2;
triangles[i * 6] = baseIndex;
triangles[i * 6 + 1] = baseIndex + 1;
triangles[i * 6 + 2] = baseIndex + 2;
triangles[i * 6 + 3] = baseIndex + 1;
triangles[i * 6 + 4] = baseIndex + 3;
triangles[i * 6 + 5] = baseIndex + 2;
}
}
Mesh mesh = new()
{
vertices = vertices,
triangles = triangles
};
mesh.RecalculateNormals();
meshFilter.mesh = mesh;
}
Now, i'm fairly sure that the way curve and mesh are created are correct, still when i run the game, the raycast hits correctly the plane, as in following image:
as you can see, the yellow sphere is correctly positioned. But when i drag the mouse, the mesh is created far distant from that point, as in following image:
Now, i tried with chatgpt too, but after several hours i can't understand why it behaves like that. I suspect it's tied to the first 4 points: in fact, the first curve is created after 4 points are recorded. The reason why it starts there and not in the starting point is beyond me. Couold you help me please? If you need more info, i can integrate...I think i put here the core. Thank you very much!
Upvotes: 0
Views: 28
Reputation: 90580
As mentioned I believe the mismatch comes from the raycast hit being in global world space while Mesh.vertices
are in local space and the meshFilter
itself has a position offset.
In InstantiateFreeAOE
you do
_currentArea = Instantiate(meshPrefabToInstantiate, startPoint, Quaternion.identity);
// this one is a bit redundant btw
_currentArea.transform.position = startPoint;
You would have to either convert the curve points into local space first
Try
vertices[i * 2] = meshFilter.transform.InverseTransformPoint(curvePoints[i] - offset);
vertices[i * 2 + 1] = meshFilter.transform.InverseTransformPoint(curvePoints[i] + offset);
or just keep the meshFilter
positioned at 0,0,0
(and ensure no transformation whatsoever up the entire hierarchy)
Upvotes: 1