I'm trying to throw an arrow in my XNA game, but I'm having a hard time trying to realize a good parabola.
What I need:
The more you hold Enter stronger the arrow goes.
The arrow angle will be always the same, 45 degrees.
This is what I have already have:
private float velocityHeld = 1f;
protected override void Update(GameTime gameTime)
{
if (Keyboard.GetState().IsKeyDown(Keys.Enter) && !released)
{
timeHeld += velocityHeld;
holding = true;
}
else
{
if (holding)
{
released = true;
holding = false;
lastTimeHeld = timeHeld;
}
}
if (released && timeHeld > 0)
{
float alpha = MathHelper.ToRadians(45f);
double vy = timeHeld * Math.Sin(alpha);
double vx = timeHeld * Math.Cos(alpha);
ShadowPosition.Y -= (int)vy;
ShadowPosition.X += (int)vx;
timeHeld -= velocityHeld;
}
else
{
released = false;
}
}
My question is, what do I need to do to make the arrow to go bottom as it loses velocity (timeHeld) to make a perfect parabola?
The solutions discussed above rely on you calculating a new velocity on every iteration, and then calculating the delta (change) from the previous position to determine the current position.
This fits in with the normal logic of a game loop. It is however computationally more complex than it needs to be, and is possibly unstable due to rounding errors.
The simpler and more stable solution is to determine the equation for the parabola and use this to directly work out the position at time t after launch.
Let the arrow start at x=0, y=0. Let the launch velocity be v. At time t after launch the a coordinate of the arrow is x = kt, where k = v*cos(45) = v/sqrt(2).
The y position is a quadratic, y = at^2 + bt + c where we don't know a, b, c.
But when t=0, y=0 so c=0
When t=0, the initial downward velocity is v*sin(45) = v/sqrt(2)
Using a tiny bit of calculus (differentiating position to get velocity), at t=0
v/sqrt(2) = 2at + b = b
Differentiating velocity to get acceleration, we get the initial acceleration at t=0 is 2a. But the only acceleration is due to gravity, so 2a=-g where g is your gravitational constant.
Putting these two equations together, at time t
x(t) = v/sqrt(2);
y(t) = -(g/2)t^2 + vt/sqrt(2)
You know v and t, you define g, so you can work out the x and y co-ordinates at time t directly from this equation.
This is a more straightforward approach and more robust (rounding errors will not accumulate). It is how I personally do it. My hand grenades always follow perfect parabolic arcs, and do so in a computationally efficient manner.
Note: I never heard of XNA until now, but I do use C#. Let me know if this doesn't quite work, though the gist of it should be there.
In your last if-statement, after Enter key is released, you want to increase the downward velocity by a certain (small constant) amount every time you call Update (I assume increasing the y-coordinate makes things move "down" on screen). To do this, as soon as Enter is released, instead of calling double vy = timeHeld * Math.Sin(alpha) every time, save the result into a variable you can reference later, then use a bool value to keep track of when to calculate that value, which is ONLY right after Enter is released.
In other words, it goes something like this:
// extra variables
bool justReleased = false;
double vy, vx;
...
protected override void Update(GameTime gameTime)
{
// ...
if (holding)
{
released = true;
holding = false;
lastTimeHeld = timeHeld;
justReleased = true; // add this here
}
// ...
if (released && timeHeld > 0)
{
float alpha = MathHelper.ToRadians(45f);
// not local variables anymore. Also I flipped the sign - my intention is that initial vertical velocity is "upwards"
if(justReleased)
{
vy = -1 * timeHeld * Math.Sin(alpha);
vx = timeHeld * Math.Cos(alpha);
justReleased = false;
}
ShadowPosition.Y += (int)vy; // Note: I flipped this operator
ShadowPosition.X += (int)vx;
timeHeld -= velocityHeld;
// increase the downward velocity
vy += 2; // or some constant. I can't test this. :\
}
else
{
released = false;
}
}
Hopefully this works, though there might be more efficient ways to do this. Though this isn't a physics site, see this for reference ;-)
Related
Here is a simple script to demonstrate the issue. This script should move the cursor to the left for 1 second and then back to the right for 1 second. However you'll see it just moves it to the left then stops. You can flip the order and try to move right first, but you'll notice that still won't work. I've also tested with moving up / down, moving up works as expected, moving down does not.
public class dumbtestscript : MonoBehaviour
{
void Start()
{
Mouse.current.WarpCursorPosition(new Vector3(123, 123));
StartCoroutine(MoveCursorLeftThenRightCoroutine());
}
IEnumerator MoveCursorLeftThenRightCoroutine()
{
float startTime = Time.time;
while (Time.time < startTime + 1)
{
Mouse.current.WarpCursorPosition(Input.mousePosition +
(Time.deltaTime * new Vector3(-0.1f, 0)));
yield return null;
}
while (Time.time < startTime + 2)
{
Mouse.current.WarpCursorPosition(Input.mousePosition +
(Time.deltaTime * new Vector3(0.1f, 0)));
yield return null;
}
}
}
Am I misunderstanding something about how WarpCursorPosition is supposed to work?
Any advice appreciated. Thanks.
Edit: Upon further inspection it may have to do with the vector being passed to WarpCursorPosition. For example regardless of whether we use this to move left:
Mouse.current.WarpCursorPosition(Input.mousePosition + new Vector3(-0.1f, 0));
or this:
Mouse.current.WarpCursorPosition(Input.mousePosition + new Vector3(-1f, 0));
it moves to the left at the same speed. So it seems anything between -0.1 and -1 is being rounded to -1. Conversely for going right everything between 0.1 and just under 1 is being rounded to 0 which would explain why it wasn't moving in the original.
So everything is getting round down to the nearest integer for some reason? Both Input.mousePosition and the Vector3 we are adding are both Vector3 and I thought could handle float numbers so not sure why things are being rounded down to ints, if thats whats happening?
The issue
So everything is getting round down to the nearest integer for some reason?
Most probably Yes!
WarpCursorPosition makes your actual system cursor jump to the given pixel coordinates.
While inside Unity the pixel space is always provided as float for many things and mainly for making direct calculations with it, the actual system cursor uses pixels (like everything that is actually happening on the screen) which are always full int values!
So, yes, it will always (not round but rather) floor your given input to the next int (full pixel). Basically what happens if you simply use
var x = (int) (Input.mousePosition.x - 0.1f);
=> You will see that x will be exactly Input.mousePosition.x - 1 since if e.g. the current Input.mousePosition.x was 230 then you get (int) 229.9f which is 229.
In the other direction though you get e.g. 230 + 0.1f so (int) 230.1f again simply is 230.
=> Your steps would need to at least be one full pixel!
Solution
So instead of constantly read the current Input.mousePosition which underlies the afore mentioned pixel coordinates rather use and update a local vector that does not:
IEnumerator MoveCursorLeftThenRightCoroutine()
{
// Store the current mousePosition as a Vector2
// This uses floats and is not restricted to full pixel jumps
var currentPosition = Input.mousePosition;
for(var passedTime = 0f; passedTime < 1; passedTime += Time.deltaTime)
{
// simply continously add to the float vector
currentPosition += Vector2.left * speedInPixelsPerSecond * Time.deltaTime;
// Using that float vector as input may in some frames cause no movement where the clamp results
// still in the same pixel but it will catch up as soon as the vector was increased/reduced enough
Mouse.current.WarpCursorPosition(currentPosition);
yield return nnull;
}
for(var passedTime = 0f; passedTime < 1; passedTime += Time.deltaTime)
{
currentPosition += Vector2.right * speedInPixelsPerSecond * Time.deltaTime;
Mouse.current.WarpCursorPosition(currentPosition);
yield return null;
}
}
In general for the new Input system I would rather use Mouse.current.position instead of Input.mousePosition.
I don't have any experience using WarpCursorPosition but when I was experimenting to answer this thread, I felt it's not consistent & would like to advice not to use it.
Code
using System.Collections;
using UnityEngine;
using UnityEngine.InputSystem;
public class Q69189325 : MonoBehaviour
{
private void Start()
{
Mouse.current.WarpCursorPosition(Input.mousePosition);
StartCoroutine(MoveLogicCrt());
}
IEnumerator MoveLogicCrt()
{
yield return MoveCursorToDirectionCrt(Vector3.left, 150, 5);
yield return null;
yield return MoveCursorToDirectionCrt(Vector3.right, 350, 5);
}
private IEnumerator MoveCursorToDirectionCrt(Vector3 direction, float speed, float movementTime)
{
float startTime = Time.time;
while(Time.time < startTime + movementTime)
{
Vector2 newMousePosition = Input.mousePosition + (Time.deltaTime * direction * speed);
Mouse.current.WarpCursorPosition(newMousePosition);
yield return null;
}
}
}
Note:
For some reason when I put the same speed to both left & right it doesn't move right which was weird.
Execution Overview
https://gfycat.com/eventhriftyannelida
Happy coding!
I've been looking through the Source SDK and read through how it handles player collision with the space around it. Inspired by that, I've been writing a character controller that should have similar behavior.
I have code to control a Transform transform by computing its velocity vecVelocity and trying to move it along that vector. I represent the player by a capsule of radius capsuleRadius and sphere centers capsuleStart, capsuleEnd, but I haven't given it a proper collider as of yet
When trying to deal with collisions, I came up with the following function
void TryPlayerMove()
{
Vector3 transDirection;
float transMagnitude;
Vector3 translation;
Vector3 currentMotion = Vector3.zero;
int i, j, maxHits = 4; //Bump up to 4 times before giving up
RaycastHit capsule;
bool collision;
Vector3[] collNormal = new Vector3[maxHits];
float collDistance;
//Find starting translation vector
translation = vecVelocity * Time.deltaTime;
//Store initial displacement
Vector3 oldTranslation = translation;
for (i = 0; i < maxHits; i++)
{
//Find translation vector magnitude and direction
transDirection = translation.normalized;
transMagnitude = translation.magnitude;
//If we wouldn't move anyway, feel free to break the loop
if (transMagnitude == 0)
{
break;
}
//Shoot a capsule to desired endpoint
collision = Physics.CapsuleCast(capsuleStart + currentMotion, capsuleEnd + currentMotion, capsuleRadius, transDirection, out capsule, transMagnitude);
if (collision)
{
//If we hit something, hug it and take off what is left of translation in that direction
collNormal[i] = capsule.normal.normalized;
collDistance = capsule.distance;
//currentMotion += transDirection * collDistance; //WHY DOESN'T THIS WORK???
translation = ClipVector(translation*(1f-collDistance/transMagnitude), collNormal[i]);
Debug.Log(transDirection.magnitude);
//If we're going towards something we've hit before, stop moving so we don't go into weird corner loops
for (j = 0; j < i; j++)
{
if (Vector3.Dot(translation, collNormal[j]) < 0)
{
translation = Vector3.zero;
break;
}
}
}
else
{
//Just move
currentMotion += translation;
break;
}
}
//Translate the player character and take note of its velocity for future computation
vecVelocity = currentMotion / Time.deltaTime;
transform.Translate(currentMotion, Space.World);
}
and the clipping function just makes sure our vector is REALLY not pointing at the collider
Vector3 ClipVector(Vector3 inputVector, Vector3 normalVector)
{
float projection;
Vector3 outputVector;
//Determine how much to take out
projection = Vector3.Dot(inputVector, normalVector);
//Subtract the perpendicular component
outputVector = inputVector - normalVector * projection;
//Iterate once more just to make sure
float adjust = Vector3.Dot(outputVector, normalVector);
if (adjust < 0f)
{
outputVector -= normalVector * adjust;
}
return outputVector;
}
This works fine as it is, and the controls respond quite as I would expect, but with a cosmetic fault: each time the player "collides" with something, it does so stopping at a different distance from the object, I've logged it and it fluctuates around 5-10 percent of the capsule's radius away from the player.
In the TryPlayerMove() function, there is a command that I would expect to make it hug the collider, it's commented and decorated with a note of desperation
//currentMotion += transDirection * collDistance; //WHY DOESN'T THIS WORK???
Whenever I uncomment this, the controller invariably goes through absolutely any collider it touches and completely messes up whatever motion it was supposed to have. I have no idea why this happens, though.
How could I implement a functionality for my player to hug the colliders it touches, seeing this doesn't seem to work at all?
I found a hacky fix for it out of the blue. I have a float deltaGroundI use to check for ground. I changed the command to
currentMotion += transDirection * (collDistance-deltaGround/20f);
but the 20 could have been any reasonably sized number. This makes it much more consistent but introduces some jitter in player motion when I try to ram myself against a wall. Still taking more suggestions.
I had tried Mathf.Epsilon but it didn't fix anything, by the way.
I'm trying to create a smooth camera movement in 2D. The target I want to hit can potentially move a large distance in a single frame, and is not like a character moving smoothly from A to B.
I'm aware of possible solutions like using Vector2.Lerp(), but that approach only slows down nicely but speeds up abruptly.
_position = Vector2.Lerp(_position, target, 0.5f * Time.deltaTime);
I've tried implementing the "arrive" steering behaviour, but cannot make it work nicely together with acceleration - especially when the target is close to the current position.
I managed to make it work pretty well in one axis, but that approach didn't work when repeated in a second axis.
var decelerateRadius = GetDistanceFromAcceleration(acceleration, Mathf.Abs(_velocity));
var direction = target - _position;
var distance = Mathf.Abs(direction);
var a = acceleration * Time.deltaTime;
if (distance > 0.0005f)
{
if (distance < decelerateRadius.x)
{
_velocity *= distance / decelerateRadius.x;
}
else
{
_velocity += direction.x >= 0.0f ? a : -a;
}
}
else
{
_velocity = 0.0f;
}
// move tracker
_position += _velocity * Time.deltaTime;
And my method for calculating the distance based on acceleration:
private Vector2 GetDistanceFromAcceleration(float a, float vx, float vy)
{
// derived from: a = (vf^2 - vi^2) / (2 * d)
return new Vector2(vx * vx / (2.0f * a), vy * vy / (2.0f * a));
}
My last attempt was making a rolling average of the target, but it suffered the same issue as lerping.
To summarize the requirements:
Must accelerate
Must decelerate and stop at target
Must not "orbit" or in other ways swing around the target, before stopping
Target must be able to move
May be limited by a maximum velocity
Any tips, pointers og solutions on how to achieve this?
I've also asked the question over at game dev
https://gamedev.stackexchange.com/questions/170056/accelerate-decelerate-towards-moving-target-and-hitting-it
The problem with your lerp is also that you actually never reach the target position you just get very very close and small.
I thought about something like this
as long as you are already at the targets position don't move. Enable orbit mode
while not within a certain targetRadius around the target position accelerate from to maxVelocity
if getting within a certain targetRadius around the target position decelerate depending on the distance / radius will be a value between 1 and 0
To get the distance there is already Vector2.Distance you could/should use.
For the movement I would recommend Vector2.MoveTowards which also avoids overshooting of the target.
something like
public class SmoothFollow2D : MonoBehaviour
{
[Header("Components")]
[Tooltip("The target this will follow")]
[SerializeField] private Transform target;
[Header("Settings")]
[Tooltip("This may never be 0!")]
[SerializeField] private float minVelocity = 0.1f;
[SerializeField] private float maxVelocity = 5.0f;
[Tooltip("The deceleration radius around the target.\nThis may never be 0!")]
[SerializeField] private float targetRadius = 1.0f;
[Tooltip("How much speed shall be added per second?\n" +
"If this is equal to MaxVelocity you know that it will take 1 second to accelerate from 0 to MaxVelocity.\n" +
"Should not be 0")]
[SerializeField] private float accelerationFactor = 3.0f;
private float _currentVelocity;
private float _lastVelocityOutsideTargetRadius;
private bool _enableOrbit;
public bool EnableOrbit
{
get { return _enableOrbit; }
private set
{
// if already the same value do nothing
if (_enableOrbit == value) return;
_enableOrbit = value;
// Whatever shall be done if orbit mode is enabled or disabled
}
}
private void Update()
{
if (target == null) return;
var distanceToTarget = Vector2.Distance(transform.position, target.position);
// This is the threshold Unity uses for equality of vectors (==)
// you might want to change it to a bigger value in order to
// make the Camera more stable e.g.
if (distanceToTarget <= 0.00001f)
{
EnableOrbit = true;
// do nothing else
return;
}
EnableOrbit = false;
if (distanceToTarget <= targetRadius)
{
// decelerate
// This will make it slower
// the closer we get to the target position
_currentVelocity = _lastVelocityOutsideTargetRadius * (distanceToTarget / targetRadius);
// as long as it is not in the final position
// it should always keep a minimum speed
_currentVelocity = Mathf.Max(_currentVelocity, minVelocity);
}
else
{
// accelerate
_currentVelocity += accelerationFactor * Time.deltaTime;
// Limit to MaxVelocity
_currentVelocity = Mathf.Min(_currentVelocity, maxVelocity);
_lastVelocityOutsideTargetRadius = _currentVelocity;
}
transform.position = Vector2.MoveTowards(transform.position, target.position, _currentVelocity * Time.deltaTime);
}
// Just for visualizing the decelerate radius around the target
private void OnDrawGizmos()
{
if (target) Gizmos.DrawWireSphere(target.position, targetRadius);
}
}
The MinVelocity is actually necessary for the edge case when the target is moved not further than TargetRadius and lastVelocityOutsideTargetRadius si still 0. In that case no acceleration takes place so lastVelocityOutsideTargetRadius is never updated.
With the values you have to play a bit ofcourse ;)
It might not be perfect yet but I hope it is a good start point to develop that further (only looks laggy due to 15 FPS for the Gif ;) )
I'm still working on "perfect" ladder movement, and what I was able to come up with is a way to calculate the exact, specific distance the character needs to move while it is on the ladder to reach the point with which it will collide perfectly with the ground above the ladder.
I know this value is exactly specific because I allow my character to land on the ladder landing when the game starts (position.y = 3.6235), and when I print the movement value in the console I get exactly 3.6235.
I'm not sure that I'm implementing this correctly in code, though, as I've noticed that this value is still barely above "0" in the console once my character is all the way up the ladder. Is Mathf.Clamp() not the correct function to limit movement, or maybe I'm using it incorrectly?
public void ClimbUpLadder(ref Vector3 deltaMovement)
{
float rayLength = raycastOrigins.centerRayLength * 2;
RaycastHit2D[] hits = Physics2D.RaycastAll(new Vector2(raycastOrigins.center.x + deltaMovement.x,
(raycastOrigins.center.y - raycastOrigins.centerRayLength + skinWidth) + deltaMovement.y), Vector2.up,
rayLength, climbMask);
Debug.DrawRay(new Vector2(raycastOrigins.center.x + deltaMovement.x,
(raycastOrigins.center.y - raycastOrigins.centerRayLength + skinWidth) + deltaMovement.y), Vector2.up * rayLength, Color.green);
for (int i = 0; i < hits.Length; i++)
{
if (hits[i])
{
if (hits[i].collider.tag == "Ladder")
{
IsClimbingLadder = true;
}
if (i >= 1 && hits[i].collider.tag == "platformOneWay")
{
//This gives us the exact distance needed to finish climbing
GameObject platform = hits[i].collider.gameObject;
Transform platformTransform = platform.GetComponent<Transform>();
float finalMoveDistance = (platformTransform.position.y - characterTransform.position.y) + platformTransform.position.y;
deltaMovement.y = Mathf.Clamp(deltaMovement.y, 0, finalMoveDistance);
print(finalMoveDistance);
}
}
}
}
Unfortunately, after I set deltaMovement.y to this value it says that finalMoveDistance is around .9 or 1.0, so I still move slightly too far up the ladder. Do you think setting the character's transform.position.y directly is the best way to smooth out the movement? My goal is to eliminate any bounce when transitioning from climbing the ladder to walking on the ground again.
I think you're making a mistake here:
float finalMoveDistance = (platformTransform.position.y - characterTransform.position.y) + platformTransform.position.y;
Why do you add the platformTransform twice?
Try this:
float finalMoveDistance = (platformTransform.position.y - characterTransform.position.y);
(Before you read, I'm very nervous about posting here since my previous question got a lot of negative responses... Please try and be nice. I am a student and can't write "Perfect" code yet)
I'm currently trying to figure out how to make a mosquito (Texture2D) swoop downwards and then come back up to it's original position.
Currently the mosquitoes simply move left and right within the screen bounds, with no Y movement.
I've stepped it through a debugger and observed it's Y coordinate. My mosquitoes are indeed swooping down and then back upwards again... However, they do it so quickly that it isn't visible to the human eye.
Therefore I need a way to smoothly swoop them down so that it's actually visible.
I am currently unable to embed images into my posts, however I have made a GIF demonstrating the effect that I want.
Here's a link to my GIF
Things I've tried:
Copied the first line of my movement code (seen below) and changed it so that it would only affect the Y coordinates.
Result: Mosquitoes were still swooping too fast to see.
Changing my mosquitoe's velocity.Y to include a Y that isn't 0, so that my movement code will also change the Y position instead of just the X position.
Result: Game was stuck in an infinite loop since my movement code is found in the Update() function, and the code never got out of the loop so that it could update the position...
Here's the movement code I have in my Update.
The mosquitoes move all the way to the right, then all the way to the left.
internal void Update(GameTime GameTime)
{
position += velocity * (float)GameTime.ElapsedGameTime.TotalSeconds;
// Reverses the direction of the mosquito if it hits the bounds
if (position.X <= gameBoundingBox.Left ||
position.X + animationSequence.CelWidth > gameBoundingBox.Right)
{
velocity.X *= -1;
}
}
And here's the actual Swoop() code that I have...
internal void Swoop(GameTime gameTime)
{
float originalYPosition = position.Y;
bool currentlySwooping = true;
while (currentlySwooping)
{
position.Y++;
if (this.BoundingBox.Bottom >= gameBoundingBox.Bottom)
{
currentlySwooping = false;
}
}
while (!currentlySwooping && position.Y > originalYPosition)
{
position.Y--;
}
}
I don't ask questions on here too often, so I'm sorry if I'm using an incorrect format or if I've given too little information.
If you need to know anything else then please let me know.
There are many ways of implementing this, but this should do it.
float originalYPosition = position.Y;
int swoopDirection = 0;
internal void Update(GameTime GameTime)
{
/* Same update code */
if (swoopDirection != 0)
{
position.Y += swoopDirection;
if (swoopDirection == 1 && this.BoundingBox.Bottom >= gameBoundingBox.Bottom)
{
swoopDirection = -1;
}
else if (swoopDirection == -1 && position.Y <= originalYPosition)
{
swoopDirection = 0;
}
}
}
internal void Swoop()
{
swoopDirection = 1;
}
Basically, we have a variable swoopDirection that changes depending on the state of the swoop (1 when going down and -1 when going up). In the update method we check that swoopDirection != 0 (we are swooping). If we are swooping we add the direction to the Y axis and check that we aren't out of bounds. If we touch the bottom we set swoopDirection to -1 so we go up. If we are at the original position swoopDirection is set to 0 and we stop swooping.