Rotate angle to target angle via shortest side - c#

Hi and thanks for reading.
I need to change this void I wrote so that it can work with negative angles.
The goal of this function is to rotate an ANGLE towards the DIRECTION by adding INCREMENT to either clockwise or counterclockwise (+ or -).
However the problem as I said is that it does not work with numbers less than 0 or greater than 360 (2pi). I need to be able to use negative angles as well.
I tried several stuff but couldn't get it to work for a while. Can anyone lend me a hand? I'll be grateful. :D
public void ToDirection(float Increment, float Direction)
{
if (CurrentAngle != Direction)
{
float ClockwiseDifference;
float CounterClockwiseDifference;
//Clockwise
if (Direction < CurrentAngle)
{
ClockwiseDifference = CurrentAngle - Direction;
}
else
{
ClockwiseDifference = Constants.Rotation_360 - (Direction - CurrentAngle);
}
//CounterClockwise
if (Direction > CurrentAngle)
{
CounterClockwiseDifference = Direction - CurrentAngle;
}
else
{
CounterClockwiseDifference = Constants.Rotation_360 - (CurrentAngle - Direction);
}
float CurrentFaceSpeed = Increment;
if (ClockwiseDifference == CounterClockwiseDifference)
{
if (Globals.Randomizer.Next(0, 2) == 0)
{
if (ClockwiseDifference < CurrentFaceSpeed)
{
CurrentAngle = Direction;
}
else
{
CurrentAngle -= CurrentFaceSpeed;
}
}
else
{
if (CounterClockwiseDifference < CurrentFaceSpeed)
{
CurrentAngle = Direction;
}
else
{
CurrentAngle += CurrentFaceSpeed;
}
}
}
else if (ClockwiseDifference < CounterClockwiseDifference)
{
if (ClockwiseDifference < CurrentFaceSpeed)
{
CurrentAngle = Direction;
}
else
{
CurrentAngle -= CurrentFaceSpeed;
}
}
else
{
if (CounterClockwiseDifference < CurrentFaceSpeed)
{
CurrentAngle = Direction;
}
else
{
CurrentAngle += CurrentFaceSpeed;
}
}
}
if (CurrentAngle >= Constants.Rotation_360)
{
CurrentAngle -= Constants.Rotation_360;
}
else if (CurrentAngle < 0)
{
CurrentAngle += Constants.Rotation_360;
}
}

Simply unwrap the angle. Then you'll always have angles from 0 to 360. Unwrap the starting angle and the target angle, then perform your turn. Here's a working example (the only method you really need is UnwrapAngle()).
internal class Program {
private static object UnwrapAngle(double angle) {
if (angle >= 0) {
var tempAngle = angle % 360;
return tempAngle == 360 ? 0 : tempAngle;
}
else
return 360 - (-1 * angle) % 360;
}
private static void TestUnwrap(double angle, double expected) {
Console.WriteLine(String.Format("{0} unwrapped = {1}, expected {2}", angle, UnwrapAngle(angle), expected));
}
private static void Main(string[] args) {
TestUnwrap(0, 0);
TestUnwrap(360, 0);
TestUnwrap(180, 180);
TestUnwrap(360 + 180, 180);
TestUnwrap(-270, 90);
TestUnwrap(-270 - 720, 90);
TestUnwrap(-725, 355);
Console.ReadLine();
}
}

This answer seems to cover the topic quite well:
Work out whether to turn clockwise or anticlockwise from two angles
RE: angles that are less than 0 or greater than 360. Basically, -10 is the same as 350. 720 is the same as 360. So if you translate the incoming angle so that it lies between 0 and 360, all your problems are solved (presuming your code works for values between 0 & 360 as you suggest).
This is something I've done myself (in a different language before):
var wantDir;
var currDir;
var directiondiff;
var maxTurn;
// want - this is your target direction \\
wantDir = argument0;
// max turn - this is the max number of degrees to turn \\
maxTurn = argument1;
// current - this is your current direction \\
currDir = direction;
if (wantDir >= (currDir + 180))
{
currDir += 360;
}
else
{
if (wantDir < (currDir - 180))
{
wantDir += 360;
}
}
directiondiff = wantDir - currDir;
if (directiondiff < -maxTurn)
{
directiondiff = -maxTurn
}
if (directiondiff > maxTurn)
{
directiondiff = maxTurn
}
// return the resultant directional change \\
return directiondiff

Related

Collision correction pushing player in the wrong direction

I'm currently working on a 2D game using an ECS with a character that's able to move left and right and make a small jump. I'm using a velocity component to control the player's movement, which is updated like this during every game tick:
private const float MaximumVerticalVelocity = 15f;
private const float VerticalAcceleration = 1f;
private const float MaximumHorizontalVelocity = 10f;
private const float HorizontalAcceleration = 1f;
private void move(GameTime gameTime, int entityID) {
float speed = HorizontalAcceleration;
var hitbox = (RectangleF) _colliderMapper.Get(entityID).Bounds;
var keyInputs = _inputMapper.Get(entityID);
var velocity = _velocityMapper.Get(entityID);
var position = _positionMapper.Get(entityID);
updateVelocity(velocity, 0, VerticalAcceleration);
if (Keyboard.GetState().IsKeyDown(keyInputs[PlayerAction.Jump]) &&
hitbox.Bottom < Game.Main.MapHeight && hitbox.Bottom > 0 && (
!Game.Main.TileIsBlank(position.X, hitbox.Bottom) ||
!Game.Main.TileIsBlank(hitbox.Right - 1, hitbox.Bottom)
)) {
velocity.DirY = -11f;
}
if (Keyboard.GetState().IsKeyDown(keyInputs[PlayerAction.Sprint])) {
speed *= 2;
}
bool leftDown = Keyboard.GetState().IsKeyDown(keyInputs[PlayerAction.Left]);
bool rightDown = Keyboard.GetState().IsKeyDown(keyInputs[PlayerAction.Right]);
if (leftDown && !(rightDown && velocity.DirX <= 0)) {
updateVelocity(velocity, -speed, 0);
}
else if (!leftDown && velocity.DirX <= 0) {
updateVelocity(velocity, speed, 0, 0, MaximumVerticalVelocity);
}
if (rightDown && !(leftDown && velocity.DirX >= 0)) {
updateVelocity(velocity, speed, 0);
}
else if (!rightDown && velocity.DirX >= 0) {
updateVelocity(velocity, -speed, 0, 0, MaximumVerticalVelocity);
}
if (!leftDown && !rightDown && velocity.DirX != 0) {
if (velocity.DirX < 0) {
updateVelocity(velocity, speed, 0, 0, MaximumVerticalVelocity);
}
else {
updateVelocity(velocity, -speed, 0, 0, MaximumVerticalVelocity);
}
}
position.X += velocity.DirX;
position.Y += velocity.DirY;
}
private void updateVelocity(Velocity velocity, float x, float y, float xLimit, float yLimit) {
if ((x >= 0 && velocity.DirX + x < xLimit) || (x < 0 && velocity.DirX + x > -xLimit)) {
velocity.DirX += x;
}
else {
if (x >= 0) {
velocity.DirX = xLimit;
}
else {
velocity.DirX = -xLimit;
}
}
if ((y >= 0 && velocity.DirY + y < yLimit) || (y < 0 && velocity.DirY + y > -yLimit)) {
velocity.DirY += y;
}
else {
if (y >= 0) {
velocity.DirY = yLimit;
}
else {
velocity.DirY = -yLimit;
}
}
}
private void updateVelocity(Velocity velocity, float x, float y) =>
updateVelocity(velocity, x, y, MaximumHorizontalVelocity, MaximumVerticalVelocity);
The player and every tile are in a collision system provided by the framework (MonoGame.Extended). All of them have rectangular hitboxes. This is the current code I'm using to resolve collisions when the player collides with a tile:
private void onCollision(int entityID, object sender, CollisionEventArgs args) {
if (args.Other is StaticCollider) {
var velocity = _velocityMapper.Get(entityID);
var position = _positionMapper.Get(entityID);
var collider = _colliderMapper.Get(entityID);
var intersection = collider.RectBounds.Intersection((RectangleF) args.Other.Bounds);
var otherBounds = (RectangleF) args.Other.Bounds;
if (intersection.Height > intersection.Width) {
if (collider.RectBounds.X < otherBounds.Position.X) {
position.X -= intersection.Width;
}
else {
position.X += intersection.Width;
}
velocity.DirX = 0;
}
else {
if (collider.RectBounds.Y < otherBounds.Y) {
position.Y -= intersection.Height;
}
else {
position.Y += intersection.Height;
}
velocity.DirY = 0;
}
collider.RectBounds.X = position.X;
collider.RectBounds.Y = position.Y;
}
}
The issue is that when the player jumps and lands on the tile in such a way that the width of the intersection is shorter than the height, the player is pushed sideways rather than upwards. (shown here and here) What do I do in this situation?
The line of code:
if (intersection.Height > intersection.Width)
Only works if the rectangles are:
Square: Check
Same size: Problem here.
Done properly his gives the following four collision zones, formed from the blue diagonals:
This is the actual test you are performing (not to scale):
The reason it moves to the right is the order the collision checks occur.
Solution
Since the aspect ratio is the same, width / height = 1, for both objects:
// ...
var adj = (float)collider.RectBounds.Width / otherBounds.Height;
if (intersection.Height * adj > intersection.Width) {
// ...
If they are not the same aspect ratio: Add a second adj2 variable:
// ...
var adj = (float)collider.RectBounds.Width / otherBounds.Height;
var adj2 = (float)otherBounds.Height / collider.RectBounds.Width;
if (intersection.Height * adj > intersection.Width * adj2) {
// ...
I will say that your programming style/approach/methodology, ECS, does not scale beyond small games.
In C# put the methods inside of the classes they will be used in or in a base class and derive classes from it, if they are not related use an interface.
Mappers are not needed, the just take up space and time being on the heap.
Function calls are expensive:
If a multiple calls to the same function, Keyboard.GetState() returns the same value store the value in a local variable.
Minimize the number of casts and dots, .: like args.Other.Bounds.
The parameter CollisionEventArgs args needs to be simplified to: Rectanglef otherBounds

Unity: Switching from one FixedUpdate-like coroutine to another one

I have a coroutine DefaultFixedUpdate() that functions as a replacement for FixedUpdate() and another coroutine called WallSlidingFixedUpdate(). I have two variables that store which of the both is active. activeCoroutine and prevActiveCoroutine with the starting strings nameof(DefaultFixedUpdate) and "no coroutine". In FixedUpdate() from Unity I have this code that should change coroutines if activeCoroutine and prevActiveCoroutine are not the same:
void FixedUpdate()
{
if (activeCoroutine != prevActiveCoroutine)
{
print($"changing coroutines from {prevActiveCoroutine} to {activeCoroutine}");
StopCoroutine(prevActiveCoroutine);
StartCoroutine(activeCoroutine);
prevActiveCoroutine = activeCoroutine;
print("changed coroutines");
}
}
When starting the game DefaultFixedUpdate() starts as expected and prevActiveCoroutine changes as expected to activeCoroutine and when a condition inside DefaultFixedUpdate() is met to switch to WallSlidingFixedUpdate() activeCoroutine changes to the new string as expected. But when StartCoroutine(activeCoroutine) gets called the code inside WallSlidingFixedUpdate() doesn't run for some reason. StopCoroutine(prevActiveCoroutine) does work. The end result is that none of the two corutines runs but expected was that WallSlidingCoroutine() runs.
These are the two Coroutines. The changing of activeCoroutine happens almost at the bottom of both functions.
IEnumerator DefaultFixedUpdate()
{
while (true)
{
//setup
gravity = (velocity.y < 0 || !Input.GetKey(KeyCode.Space)) ? dropGravity : normalGravity;
Vector2 pos = transform.position + col.size.y * 0.5f * Vector3.up;
Vector2 timeStepVelocity = velocity * Time.fixedDeltaTime;
Vector2 pos1 = pos;
velocity.y -= gravity * Time.fixedDeltaTime;
//checking collision
RaycastHit2D[] colChecks = Physics2D.BoxCastAll(pos, col.size, 0, timeStepVelocity, timeStepVelocity.magnitude, 0b1000000);
//remove unwanted collisions that are behind and don't make sense to collide with
List<RaycastHit2D> newColChecks = new();
foreach (RaycastHit2D colCheck in colChecks)
if (Vector2.Dot(colCheck.normal, timeStepVelocity) < 0)
newColChecks.Add(colCheck);
colChecks = newColChecks.ToArray();
//remove unwanted velocities
foreach (RaycastHit2D colCheck in colChecks)
{
if (Mathf.Abs(colCheck.normal.x / colCheck.normal.y) > 1) // left and right
{
velocity.x = Mathf.Sign(colCheck.normal.x) > 0 ? Mathf.Max(velocity.x, 0) : Mathf.Min(velocity.x, 0);
}
else // top bottom
{
velocity.y = Mathf.Sign(colCheck.normal.y) > 0 ? Mathf.Max(velocity.y, 0) : Mathf.Min(velocity.y, 0);
}
}
//calculate correct destination
if (colChecks.Length == 0)
pos += timeStepVelocity;
else
{
//create bounding box for all possible future positions
Vector4 posBox = new Vector4(pos.x, pos.x + timeStepVelocity.x, pos.y, pos.y + timeStepVelocity.y);
//move to surface
pos = colChecks[0].centroid;
//slide along surface until hitting another tangent or the bounding box
Vector2 tangent = new Vector3(colChecks[0].normal.y, -colChecks[0].normal.x);
Vector2 centroid = pos;
//slide until hitting distance bounds
if (Mathf.Abs(tangent.x) > Mathf.Abs(tangent.y)) //top and bottom
{
pos += tangent / tangent.x * (timeStepVelocity.x - pos.x + pos1.x);
tangent = Mathf.Sign(centroid.y - pos1.y) * tangent;
}
else //left and right
{
pos += tangent / tangent.y * (timeStepVelocity.y - pos.y + pos1.y);
tangent = Mathf.Sign(centroid.x - pos1.x) * tangent;
}
//save previous surface that is being slid on
Vector2 prevTangent = Tangent(colChecks[0].normal);
//check if I collided with another tangent along the way
colChecks = Physics2D.BoxCastAll(centroid, col.size, 0, pos, Vector2.Distance(pos, centroid), 0b1000000);
//remove unwanted collisions that are behind and don't make sense to collide with
newColChecks = new();
foreach (RaycastHit2D colCheck in colChecks)
if (Vector2.Dot(colCheck.normal, tangent * Mathf.Sign(Vector2.Dot(pos - centroid, tangent))) < 0 || Vector2.Dot(colCheck.normal, timeStepVelocity) < 0)
newColChecks.Add(colCheck);
colChecks = newColChecks.ToArray();
if (colChecks.Length > 1)
{
pos = centroid + (pos - centroid).normalized * colChecks[1].distance;
}
//repeat
int loopBreak = 0;
string loopBreakMessage = "";
while (true)
{
loopBreakMessage += centroid + "\n";
if (loopBreak > 4)
{
Debug.LogError("loop limit exceeded\n" + loopBreakMessage);
break;
}
loopBreak++;
//check if I collided with another tangent along the way
colChecks = Physics2D.BoxCastAll(centroid, col.size, 0, pos - centroid, Vector2.Distance(pos, centroid), 0b1000000);
//remove unwanted collisions that are behind and don't make sense to collide with
newColChecks = new();
foreach (RaycastHit2D colCheck in colChecks)
if (Tangent(colCheck.normal) != prevTangent)
newColChecks.Add(colCheck);
colChecks = newColChecks.ToArray();
if (colChecks.Length > 2)
{
prevTangent = colChecks[0].normal;
if (!VectorOnSameQuarter(colChecks[1].normal, colChecks[2].normal)) // stuck in a corner
{
pos = centroid + (pos - centroid).normalized * colChecks[2].distance;
break;
}
else //continue along new tangent
{
//corner pos
centroid += (pos - centroid).normalized * colChecks[2].distance;
//end pos if there's no surface along the way
pos = PosAlongVectorInsideRectangle(posBox, centroid, Tangent(colChecks[2].normal));
}
}
else
{
break;
}
}
AlignVelocityWithGround();
}
//check if player is grounded and save ground tangent to remove small hops when going down slopes
PlayerState.OnGround = false;
RaycastHit2D[] jumpChecks = Physics2D.BoxCastAll(pos, col.size, 0, Vector2.down, 1e-3f, 0b1000000);
if (jumpChecks.Length != 0)
{
foreach (RaycastHit2D jumpCheck in jumpChecks)
{
if (jumpCheck.normal.y > Mathf.Abs(jumpCheck.normal.x))
{
PlayerState.OnGround = true;
groundTangent = new Vector2(jumpCheck.normal.y, -jumpCheck.normal.x);
}
}
PlayerState.state = PlayerState.state == PlayerState.State.WallSliding ? PlayerState.State.Default : PlayerState.state;
//check for a wall slide and save wall tangent for smooth sliding
if (!PlayerState.OnGround && velocity.y < 0 && Mathf.Abs(jumpChecks[0].normal.x) > Mathf.Abs(jumpChecks[0].normal.y) && Mathf.Asin(jumpChecks[0].normal.y) * Mathf.Rad2Deg < 15)
{
groundTangent = new Vector2(jumpChecks[0].normal.y, -jumpChecks[0].normal.x);
transform.position = pos - col.size.y * 0.5f * Vector2.up;
activeCoroutine = nameof(WallSlidingFixedUpdate);
yield break;
}
}
transform.position = pos - col.size.y * 0.5f * Vector2.up;
yield return new WaitForFixedUpdate();
}
}
IEnumerable WallSlidingFixedUpdate()
{
print("wallsliding coroutine entered");
while (true)
{
print("wallsliding coroutine happening");
Vector2 pos = transform.position;
pos -= Time.fixedDeltaTime * wallSlideSpeed * groundTangent / groundTangent.y;
transform.position = pos;
if (InputManager.Left && groundTangent.y > 0)
{
activeCoroutine = nameof(DefaultFixedUpdate);
yield break;
}
else if (InputManager.Right)
{
activeCoroutine = nameof(DefaultFixedUpdate);
yield break;
}
yield return new WaitForFixedUpdate();
}
}
You figured your issue about the return type out already -> It needs to be IEnumerator.
Anyway my question is WHY?
Instead of going through all that trouble for switching between different Coroutine instances via a string I would rather have only a single outer scope routine running and then rather switch between which inner block to execute
private enum RoutineType
{
Default,
WallSliding
}
private RouineType activeRoutine;
private IEnumerator FixedUptadeRoutine()
{
while(true)
{
switch(activeRoutine)
{
case RoutineType.WallSliding:
yield return DefaultRoutine();
break;
case default:
yield return WallSlidingRoutine();
break;
}
}
}
private IEnumerator DefaultRoutine()
{
while(true)
{
// NOTE: Btw you want to wait for fixed update FIRST
// Before applying all the values
...
// And now this will simply make sure you change the routine type
activeRoutine = RoutineType.WallSliding;
// and exit this one so the outer routine will handle the switch in the next frame
yield break;
...
}
}
private IEnumerator WallSlidingRoutine()
{
...
}
you can simply yield return any IEnumerator so it will be executed and at the same time the outer routine waits for it to finish. There is no need to have multiple start and stop coroutines.
Holy moly, how did I miss this. The return value of WallSlidingFixedUpdate is IEnumerable and not IEnumerator. I changed it to IEnumertor and now it works.

How to clamp camera rotation axis correctly?

I am trying to clamp the X and Y axis of my camera, which I have managed to do. However, when the clamped value reaches the MAX threshold it will jump back to the MIN threshold!
Any idea what is causing this in my code?
private void ClimbingLookRotation()
{
if (input.mouseX != 0 || input.mouseY != 0f)
{
orientation.rotation *= Quaternion.AngleAxis(input.mouseX, Vector3.up);
orientation.rotation *= Quaternion.AngleAxis(input.mouseY, Vector3.right);
}
var rotX = orientation.eulerAngles.x;
var rotY = orientation.eulerAngles.y;
rotX = Mathf.Clamp(rotX, 1, 25);
rotY = Mathf.Clamp(rotY, 200, 355);
orientation.eulerAngles = new Vector3(rotX, rotY, 0);
}
Any help would be appreciated!
Thankyou.
I found something the works well so thought I'd answer in case it would help anyone!
public static float ClampAngle(float angle, float min, float max)
{ //Normalises angle value passed in between -180 to 180 to make the angle clampable
angle = NormalizeAngle(angle);
if (angle > 180)
{
angle -= 360;
}
else if (angle < -180)
{
angle += 360;
}
min = NormalizeAngle(min);
if (min > 180)
{
min -= 360;
}
else if (min < -180)
{
min += 360;
}
max = NormalizeAngle(max);
if (max > 180)
{
max -= 360;
}
else if (max < -180)
{
max += 360;
}
return Mathf.Clamp(angle, min, max);
}
public static float NormalizeAngle(float angle)
{ //If the angle is above or below 360 degrees, normalise it
while (angle > 360)
angle -= 360;
while (angle < 0)
angle += 360;
return angle;
}
And then just call the ClampAngle method where ever you need to clamp a value, for example:
private void ClimbingLookRotation()
{
if (input.mouseX != 0 || input.mouseY != 0f)
{
orientation.rotation *= Quaternion.AngleAxis(input.mouseX, Vector3.up);
orientation.rotation *= Quaternion.AngleAxis(input.mouseY, Vector3.right);
}
var rotX = orientation.eulerAngles.x;
var rotY = orientation.eulerAngles.y;
rotX = HelperFunctions.ClampAngle(rotX, -10, 25); //Here
rotY = HelperFunctions.ClampAngle(rotY, 200, 340); //And here
orientation.eulerAngles = new Vector3(rotX, rotY, 0);
}
I've called it in my camera rotation function to clamp both rotX and rotY, which is later applied to the rotation of my orientation game object.
Thankyou again to Ruzihm for your help before :)
You could re-center your euler angles around the center value then clamp the difference:
float centralX = 13f;
float extentX = 12f;
float centralY = 277.5f;
float extentY = 77.5f;
private static float ClampEuler(float val, float center, float extent)
{
return center + Mathf.Clamp((val - center + 360f) % 360f, -extent, extent);
}
private void ClimbingLookRotation()
{
if (input.mouseX != 0 || input.mouseY != 0f)
{
orientation.rotation *= Quaternion.AngleAxis(input.mouseX, Vector3.up);
orientation.rotation *= Quaternion.AngleAxis(input.mouseY, Vector3.right);
}
var rotX = orientation.eulerAngles.x;
var rotY = orientation.eulerAngles.y;
rotX = ClampEuler(rotX, centralX, extentX);
rotY = ClampEuler(rotY, centralY, extentY);
orientation.eulerAngles = new Vector3(rotX, rotY, 0);
}
By the way you may want to do orientation.rotation *= Quaternion.AngleAxis(input.mouseY, orientation.right); to rotate around the local right instead of global right. Rotating around global right may produce a rolling effect over time which you may not want.

Change animation based on mouse screen position in Unity 2D

I am developing an Isometric 2D game in Unity, using C# scripts. The character will be able to run in 8 different orientations.
I am trying to trigger a running animation depending on the mouse position.
My script is working fine but I don't think is the best way to face this problem.
First of all, I have an enum with the possible orientations:
public enum Orientations {N,NE,E,SE,S,SW,W,NW,NONE}
I wrote a method that returns an Orientations value based in a movement. This is because I want to trigger an animation based on the movement, so the Character will always be looking at the direction of the movement:
public static Orientations GetOrientation(Vector2 movement)
{
if (movement.x == 0 && movement.y == 1)
{
return Orientations.N;
}
else if (movement.x == 1 && movement.y == 0)
{
return Orientations.E;
}
else if (movement.x == 0 && movement.y == -1)
{
return Orientations.S;
}
else if (movement.x == -1 && movement.y == 0)
{
return Orientations.W;
}
else if (movement.x == -1 && movement.y == 1)
{
return Orientations.NW;
}
else if (movement.x == 1 && movement.y == 1)
{
return Orientations.NE;
}
else if (movement.x == -1 && movement.y == -1)
{
return Orientations.SW;
}
else if (movement.x == 1 && movement.y == -1)
{
return Orientations.SE;
}
return Orientations.NONE;
}
Next, I get the mouse angle between the character and the screen.
public static float GetMousePosition(Transform transform)
{
float cameraDistance = Camera.main.transform.position.y - transform.position.y;
Vector3 mousePosition = Camera.main.ScreenToWorldPoint(new Vector3(Input.mousePosition.x, Input.mousePosition.y, cameraDistance));
float angleRadius = Mathf.Atan2(mousePosition.y - transform.position.y, mousePosition.x - transform.position.x);
float angle = (180 / Mathf.PI) * angleRadius;
angle = (angle < 0) ? angle + 360 : angle;
return angle;
}
Then, I transform the angle in a Vector2, so I am able to switch between triggering animations by the character movement and mouse position:
public static Vector2 AngleToVectorDirection(Transform transform)
{
Vector2 direction = new Vector2(0,0);
float angle = GetMousePosition(transform);
if(angle >= 67.5 && angle < 112.5)
{
direction = new Vector2(0,1);
}
else if (angle >= 112.5 && angle < 157.5)
{
direction = new Vector2(-1,1);
}
else if (angle >= 157.5 && angle < 202.5)
{
direction = new Vector2(-1, 0);
}
else if (angle >= 202.5 && angle < 247.5)
{
direction = new Vector2(-1, -1);
}
else if (angle >= 247.5 && angle < 292.5)
{
direction = new Vector2(0, -1);
}
else if (angle >= 292.5 && angle < 337.5)
{
direction = new Vector2(1, -1);
}
else if (angle >= 337.5 || angle < 22.5)
{
direction = new Vector2(1, 0);
}
else if (angle >= 22.5 && angle < 67.5)
{
direction = new Vector2(1, 1);
}
return direction;
}
To finish, I return the Orientation as I mentioned:
public static Orientations GetOrientationByMovement(Transform transform, Vector2 movement)
{
Vector2 orientation;
if (!Input.GetButton("Fire1"))
{
orientation = movement;
}
else
{
orientation = AngleToVectorDirection(transform);
}
return GetOrientation(orientation);
}
This Orientation is received by an AnimationController script that triggers the animation.
I can not simply rotate the character, or flip sprite, or something like that because it is animation based.
Here you are doing the work twice. So in an optimisation point of view that not ideal but in a design point of view that can be good to separate the works. The question is where does the movement parameter come from in the GetOrientationByMovement method? can you use instead the orientation enum? if yes then that simplifies your code greatly!
You end up with :
public static Orientations AngleToVectorDirection(Transform transform)
{
float angle = GetMousePosition(transform);
if(angle >= 67.5 && angle < 112.5)
{
return Orientations.N;
}
else if (angle >= 112.5 && angle < 157.5)
{
return Orientations.NW;
}
else if (angle >= 157.5 && angle < 202.5)
{
return Orientations.W;
}
else if (angle >= 202.5 && angle < 247.5)
{
return Orientations.SW;
}
else if (angle >= 247.5 && angle < 292.5)
{
return Orientations.S;
}
else if (angle >= 292.5 && angle < 337.5)
{
return Orientations.SE;
}
else if (angle >= 337.5 || angle < 22.5)
{
return Orientations.E;
}
else if (angle >= 22.5 && angle < 67.5)
{
return Orientations.NE;
}
}
Also try to keep consistency in your code. if you use return value in the if statement do that for both function, in you use it outside do it everywhere.
Note that in your implementation of the AngleToVectorDirection you don't need to create a new vector in the beginning since you cover all the angle that can be.
Answering my own question:
With the just released unity's 3.8f1 patch, I've found in them demo project a way to trigger animations in a really simple way.
I am just using the code you can find in the official site:
https://blogs.unity3d.com/2019/03/18/isometric-2d-environments-with-tilemap/?_ga=2.120446600.1010886114.1552829987-288556513.1552829987
They use a IsometricCharacterRenderer script where use Animator.Play() passing as parameter a value of a string[], based on the player movement.
public static readonly string[] staticDirections = { "Static N", "Static NW", "Static W", "Static SW", "Static S", "Static SE", "Static E", "Static NE" };
public static readonly string[] runDirections = { "Run N", "Run NW", "Run W", "Run SW", "Run S", "Run SE", "Run E", "Run NE" };
public void SetDirection(Vector2 direction)
{
//use the Run states by default
string[] directionArray = null;
//measure the magnitude of the input.
if (direction.magnitude < .01f)
{
//if we are basically standing still, we'll use the Static states
//we won't be able to calculate a direction if the user isn't pressing one, anyway!
directionArray = staticDirections;
}
else
{
//we can calculate which direction we are going in
//use DirectionToIndex to get the index of the slice from the direction vector
//save the answer to lastDirection
directionArray = runDirections;
lastDirection = DirectionToIndex(direction, 8);
}
//tell the animator to play the requested state
animator.Play(directionArray[lastDirection]);
}
And to get the direction index, they convert the movement in an angle, just how I did, but in a smart way.
public static int DirectionToIndex(Vector2 dir, int sliceCount)
{
//get the normalized direction
Vector2 normDir = dir.normalized;
//calculate how many degrees one slice is
float step = 360f / sliceCount;
//calculate how many degress half a slice is.
//we need this to offset the pie, so that the North (UP) slice is aligned in the center
float halfstep = step / 2;
//get the angle from -180 to 180 of the direction vector relative to the Up vector.
//this will return the angle between dir and North.
float angle = Vector2.SignedAngle(Vector2.up, normDir);
//add the halfslice offset
angle += halfstep;
//if angle is negative, then let's make it positive by adding 360 to wrap it around.
if (angle < 0)
{
angle += 360;
}
//calculate the amount of steps required to reach this angle
float stepCount = angle / step;
//round it, and we have the answer!
return Mathf.FloorToInt(stepCount);
}

Line of Sight with Ray in XNA/Monogame

I'm trying to create a Line of Sight method for an enemy class. However, it always returns false, no matter how close the player is to the enemy or whether the ray passes through any blocks to get to the player.
public virtual bool PlayerInLOS()
{
Vector3 middleOfPlayer = new Vector3(Level.Player.Position.X, Level.Player.Position.Y - Level.Player.BoundingRectangle.Height / 2, 0);
Vector3 middleOfEnemy = new Vector3(Position.X, Position.Y - localBounds.Height / 2, 0);
Vector3 direction = middleOfPlayer - middleOfEnemy;
float distanceToPlayer = Vector3.Distance(middleOfEnemy, middleOfPlayer);
if (direction != Vector3.Zero)
direction.Normalize();
Ray lineOfSight = new Ray(middleOfEnemy, direction);
float? lineToPlayer = lineOfSight.Intersects(Level.Player.BoundingBox);
foreach (BoundingBox box in Level.boundingBoxes)
{
float? distanceToIntersect = lineOfSight.Intersects(box);
if (distanceToIntersect == null)
continue;
else if (distanceToIntersect < visionLength && distanceToIntersect < distanceToPlayer && distanceToIntersect != null)
return false;
}
// Never gets to this part because it always returns before it exits the for loop
if (lineToPlayer < visionLength)
return true;
else return false;
}
Any ideas? Thanks.
I ended up fixing the problem by implementing an entirely different solution: checking every Vector2 along the distance between the enemy and the player. Works perfectly.
public bool CanSeePlayer()
{
Vector2 middleOfPlayer = new Vector2(Level.Player.Position.X, Level.Player.Position.Y - Level.Player.BoundingRectangle.Height / 2);
Vector2 middleOfEnemy = new Vector2(Position.X, Position.Y - localBounds.Height / 2);
Vector2 direction = middleOfPlayer - middleOfEnemy;
float distanceToPlayer = Vector2.Distance(middleOfEnemy, middleOfPlayer);
if (visionLength > distanceToPlayer) // If the enemy can see farther than the player's distance,
{
if (direction != Vector2.Zero)
direction.Normalize();
for (int y = 0; y < Level.tiles.GetLength(1); ++y) // loop through every tile,
{
for (int x = 0; x < Level.tiles.GetLength(0); ++x)
{
if (Level.GetCollision(x, y) != TileCollision.Passable) // and if the block is solid,
{
Vector2 currentPos = middleOfEnemy;
float lengthOfLine = 0.0f;
Rectangle tileRect = new Rectangle(x * Tile.Width, y * Tile.Height, Tile.Width, Tile.Height);
while (lengthOfLine < distanceToPlayer + 1.0f) // check every point along the line
{
currentPos += direction;
if (tileRect.Contains(currentPos)) // to see if the tile contains it.
{
return false;
}
lengthOfLine = Vector2.Distance(middleOfEnemy, currentPos);
}
}
}
}
// If every tile does not contain a single point along the line from the enemy to the player,
return true;
}
return false;
}
If you need to check if enemy is within angle of sight, and in some distance you could try this code.
public static bool InLOS(float AngleDistance, float PositionDistance, Vector2 PositionA, Vector2 PositionB, float AngleB)
{
float AngleBetween = (float)Math.Atan2((PositionA.Y - PositionB.Y), (PositionA.X - PositionB.X));
if ((AngleBetween <= (AngleB + (AngleDistance / 2f / 100f))) && (AngleBetween >= (AngleB - (AngleDistance / 2f / 100f))) && (Vector2.Distance(PositionA, PositionB) <= PositionDistance)) return true;
else return false;
}
credits: https://gamedev.stackexchange.com/questions/26813/xna-2d-line-of-sight-check

Categories