How do I access specific raycast hits in a 'for' loop? - c#

I have a loop that draws 4 raycasts from the bottom of my character, which are used to detect collision with objects. My issue is that if raycast #1 and raycast #4 are colliding with different objects they return the values from both objects.
I'd like to make it so that I look for a hit on raycast #4 only, and if that doesn't return a hit then I check raycast #3, etc. Once I have a hit I will check for the value of the object with which the raycast is colliding. I tried using RaycastHits[], but I believe this is intended to be used when you want to analyze multiple hits within all raycasts, not just a single raycast.
for (int i = 0; i < VerticalRayCount; i++)
{
Vector2 rayOrigin = (directionY == -1) ? RaycastOrigin.bottomLeft : RaycastOrigin.topLeft;
rayOrigin += Vector2.right * (VerticalRaySpacing * i + deltaMovement.x);
RaycastHit2D hit = Physics2D.Raycast(rayOrigin, Vector2.up * directionY, rayLength, collisionMask);
Debug.DrawRay(rayOrigin, Vector2.up * directionY * rayLength, Color.red);
if (directionY == -1)
{
if (directionX == 1)
{
if (i == VerticalRayCount -1)
{
if (hit)
{
if (currentPlatform != hit.collider.gameObject)
{
var platform = hit.collider.gameObject.GetComponent<IPlatform>();
currentPlatform = hit.collider.gameObject;
}
}
}
else if (i == 0)
{
if (hit)
{
if (currentPlatform != hit.collider.gameObject)
{
var platform = hit.collider.gameObject.GetComponent<IPlatform>();
currentPlatform = hit.collider.gameObject;
}
}
}
}
}
}

I see two problems in your code:
In your for-loop currentPlatform gets allways overwritten by the last hit.
But since you check if(currentPlatform != hit.collider.gameObject) it will allways switch between the last two hits if there is more than one. This happens because it hits the first one and sets it. Than the second hit is not equal to the first one so it gets overwritten. So the next time the currentPlatform is still the second hit and gets again overwritten with the first hit of the new loop and always goes on like this.
To avoid the second remove those if(currentPlatform != hit.collidergameObject).
And you should add break; after a hit so the for loop doesn't continue with the other raycasts but rather exits the for loop immediately. So you get only one hit and don't overwrite them.
...
if(hit)
{
var platform = hit.collider.gameObject.GetComponent<IPlatform>();
currentPlatform = hit.collider.gameObject;
// here add break so the for loop is not continued
break;
}
...
On this way currentPlattform allways has the value of the first hit.

Related

How to smoothly turn character controller in X Direction

Overview
I'm making an endless runner game. In this game, I have 5 lines, I want to player smoothly switch lines something like this Ref Link
In my case, I have everything the same but instead of a car, I have a player with PlayerController attached to it.
I'm changing the player line on Button click and also on IPointerDownHandler & IPointerUpHandler
Code
Full Code
[SerializeField] private List<Vector3> lines; // 5 lines in my case. Vector3 (0,0,0) and so on ...
private int flag;
Vector3 currLine;
private void ChangeLines ()
{
// Getting Inputs
if (Input.GetKey(KeyCode.LeftArrow)) { flag = -1; }
else if (Input.GetKey(KeyCode.RightArrow)) { flag = 1; }
else flag = 0;
if (flag > 0) MoveRight ();
else if (flag < 0) MoveLeft ();
}
//I used two approaches to moving but both are not working as indented
// 1 _ using DoTween
// 2 _ using Vector3.Lerp ()
private void MoveRight ()
{
// some input delay for Ipointers
if (inputDelay > 0) return;
if (currLine == lines [lines.Count - 1]) return; // can't move right anymore
transform.DoRotate (new Vector3(0, 45, 0) , 0.2f); // rotate player toward target
transform.DoMoveX (currLine.X, 0.3f) // 0.3f is coming from inspector
.SetEase (Ease.Linear) // i almost tried all Ease
.OnComplete ( ()=> DoTween.DoRotate (new Vector3(0, 0, 0) , 0.2f));
// using Lerp
LookAt (new Vector3 (currLine.x,Y,Z));
transform.position = Vector3.Lerp(transform.position, new Vector3(currLine.x, ..,..), lineChangeCurve
.Evaluate(Time.deltaTime * lineChangeSpeed));
}
private void MoveLeft ()
{
// same code as MoveRight
}
Problem
The code I wrote is prettier much working. the player is changing lines and also rotating towards the line but I'm unable to figure out what should i need to do to make this effect look like a reference.
Can you tell me how can I achieve the same smoother effect as the reference for my player?
Here is the link that I made so far
3D Assets link
Player lines distance :
new Vector3 (-8, 0,0)
new Vector3 (-4, 0,0)
new Vector3 (0, 0,0)
new Vector3 (4, 0,0)
new Vector3 (8, 0,0)
Thanks in Advance
You seem to be mixing two different animation techniques.
You do
transform.DoMoveX (currLine.X, 0.3f)
.SetEase (Ease.Linear)
...
which starts a tween animation
and then you also do Evaluate on the lerp which seems to be a bit redundant.
Your issue with the Lerp and Evaluate is
a) that you pass in
Time.deltaTime * lineChangeSpeed
which basically means
lineChangeSpeed / frame rate (e.g. 60)
=> This is a way to small value and will most probably basically mean you don't move at all (depending on your used curve)
You want to call this every frame and pass in a value increasing from 0 to 1 here (e.g. in a Coroutine)
b) you call it only exactly once. This will not result in any movement at all .. or at least only in a single step in the first frame => which probably causes a little "hiccup"
=> get rid of the lerp line all together. At best it interferes with the tween animation and is probably the cause of it looking somewhat off
To the question about keeping the button pressed.
You have nothing to prevent this in
private void ChangeLines ()
{
// Getting Inputs
if (Input.GetKey(KeyCode.LeftArrow)) { flag = -1; }
else if (Input.GetKey(KeyCode.RightArrow)) { flag = 1; }
else flag = 0;
if (flag > 0) MoveRight ();
else if (flag < 0) MoveLeft ();
}
First of all the flag seems quite redundant. And then you would
Either change to GetKeyDown so only the first frame where the button goes down is handled
Or add a flag that ignores all input while an animation is already running
Or even both (depending on your desired UX)
So e.g.
private bool alreadyAnimating;
private void ChangeLines ()
{
if(alreadyAnimating) return;
if (Input.GetKey(KeyCode.LeftArrow))
// Or as said maybe even better
if (Input.GetKeyDown(KeyCode.LeftArrow))
{
MoveLeft();
}
else if (Input.GetKey(KeyCode.RightArrow))
// same here
else if (Input.GetKeyDown(KeyCode.RightArrow))
{
MoveRight();
}
}
and then block and reset the flag when done with moving
alreadyAnimating = true;
transform.DoRotate (new Vector3(0, 45, 0) , 0.2f);
transform.DoMoveX (currLine.X, 0.3f)
.SetEase (Ease.Linear)
.OnComplete (()=>
{
DoTween.DoRotate (new Vector3(0, 0, 0) , 0.2f)
.OnComplete(() => { alreadyAnimating = false; })
});
I used Lerp() for these kind of conditions and it worked well all the time. As I saw your character correctly turns but it look like a snap turn. I couldn't spot an obvious error in your code. Play a little more with the rotation time and you will get your desired result. Sorry this must be a comment but dont have the reps.
The idea of the animation for switching lines is pretty simple. All you need is an animation of turning the main character to the side and back. Let's say the line change animation takes 1 second. Then you need that when moving to the left lane, the main character makes a turn to the left in 0.5 seconds and the next 0.5 seconds turns straight again. While the gameobject moves into the adjacent line, the main character will make a small smooth turn and the animation will look better. This is how it is done in the example you showed in the question. In your video, instead of this animation, the main character abruptly turns to a certain angle and returns to its original position abruptly back when it reaches the required line.

if statement not exiting properly C# and Unity

I am trying to get my 'player' to change directions with my below script
however, it is contently staying stuck in the first 2 if statements of my 'void Update()'
I am trying to use these two scripts (1. https://pastebin.com/AGLatvUD (I wrote this one) and 2. https://pastebin.com/2XA3w04w)
I am attempting to use CharacterController2D to move my player with specified points and actions
Don't know if this is the right place to ask but I figured I'd try!!
void Update() // not sure if should have more in 'FixedUpdate' or others (maybe?)
{
if (isRight && transform.position.x > currentPoint.position.x) // flipping the character -- I'm pretty sure I can use TestCharacterController2D to do this for me, this is comparing the player's 'transform'
{
moveSpeed = -0.25f; // tells controller to head in the left direction
isRight = false; // no longer facing right
}
if (!isRight && transform.position.x < currentPoint.position.x) // reverse of above
{
moveSpeed = -0.25f; // tells controller to head in the right direction
isRight = true; // no longer facing left
}
if (transform.position == currentPoint.position) // checks to see if player is at 'currentPoint'
{
pause = true; // starts the pause sequenece
if (pause) // when the movement is pause do the the following
{
animator.SetFloat("Speed", 0); // player stops moving -- works!
if (maxPause <= 100) // checks to see if still paused
{
Debug.Log(maxPause);
maxPause--; // reduce pause amount (working way out of loop)
if (maxPause < 0) // found 'maxPause' was going to far below zero
maxPause = 0;
}
if (maxPause == 0) // when 'maxPause' timer has finished
{
pointsSelection++; // move to netx point
maxPause = 100; // reset 'maxPause' timer
pause = false; // resume 'transform.position == currentPoint.position' process
}
}
if (pointsSelection == points.Length) // makes sure 'pointsSelection' doesn't go out of bounds
pointsSelection = 0; // start the player's movement process over again
}
else // not sure if requried
Debug.Log("removed pause = false");
any help would be appreciated!!!
Thank you very much!!
I am an amateur (obviously :))
littlejiver
There's nothing that sets the character's position exactly to currentPoint. There are a number of ways to solve this. One way is to check if the character is near the currentPoint instead of exactly on it.
if (Vector2.Distance(transform.position, currentPoint.position) < 0.1f) {
pause = true; // starts the pause sequenece
...

Snake Program stops running without exception

I am trying to program snake in console. After a random amount of steps the snake takes, a new GameObject spawns (e.g. an apple that makes the snake grow or a mushroom which makes the snake faster).
I create a random gameObject (on random (left, top) coordinates and then I need to check if that gameObject spawns on top of a snakeBodyPart or on top of another gameObject. If that is the case, I create a new gameObject and try the same again. Only if it doesn´t collide with another gameObject or the Snake, it will spawn.
snakeElemnts = a list, which has all the snake- body-parts;
gameObjects = a list with all existing gameObjects;
If the noCollisionAmount == the amount of snakeElements and GameObjects, there should be no collision and the new GameObject should spawn.
Unfortunately at one point (early on), the snake just stops moving and nothing happens (no exception or anything).
I can´t debug because I have a KeyboardWatcher running, which checks if a key gets pressed. Therefore when I press break, I can only examine the keyboardwatcher.
Setting a breakpoint is not useful, because I never break when the problem arises.
if (this.randomStepNumber == 0)
{
int noCollision = 0; // this variable counts the amount of times there was no collision
GameObject validGameObject;
while (true)
{
validGameObject = this.snakeGameObjectFactory.Generate();
foreach (SnakeElements element in snakeElements)
{
if (validGameObject.XPosition != element.XPosition || validGameObject.YPosition != element.YPosition)
{
noCollision++;
}
else
{
break;
}
}
foreach (GameObject gameObject in gameObjects)
{
if (noCollision == snakeElements.Count) // if there was no collision of the new gameobject with an element of the snake, the gameobjects will get checked
{
if (validGameObject.XPosition != gameObject.XPosition || validGameObject.YPosition != gameObject.YPosition)
{
noCollision++;
}
else
{
break;
}
}
else
{
break;
}
}
if (noCollision == snakeElements.Count + gameObjects.Count) // if there was no collision at all, the check is ended
{
break;
}
else
{
noCollision = 0;
}
}
this.gameObjects.Add(validGameObject);
this.randomStepNumber = this.random.Next(10, 30);
}
Based on your comments that removing this block of code causes the problem to cease, I thought maybe it would be good to clean up the code a little and remove that potential infinite loop with the continue statement. Making use of a little LINQ Enumerable.Any and a do-while loop, we can simplify the logic of the above code. There isn't really a need to be counting collisions, since the purpose of the code is to create a new object iff a collision is detected. That means if we detect a collision with the snake, then we want to generate a new object, or if we detect a collision with another existing object, then we want to generate a new object. So instead of counting, we use the Any statement to check if there is any collision with the snake or an existing object. If there is, then generate a new object. If not, then use that object and continue. Note: In order to use the Enumerable.Any you will need to add a using statement for the namespace System.Linq.
if (randomStepNumber == 0)
{
GameObject validGameObject;
do
{
validGameObject = snakeGameObjectFactory.Generate();
}
while(snakeElements.Any(s => validGameObject.XPosition == s.XPosition && validGameObject.YPosition == s.YPosition) ||
gameObjects.Any(o => validGameObject.XPosition == o.XPosition && validGameObject.YPosition == o.YPosition));
gameObjects.Add(validGameObject);
randomStepNumber = random.Next(10, 30);
}
As a side note. I removed the this keyword as it appeared you were not using it in all cases in your code, and if you do not have to use it, it makes the code a little more readable because it is less wordy. If it needs to be added back because of variable collisions, then I would suggest renaming the variables as having member and local variables with the same name can also be confusing when trying to debug something.
Secondary note - here is a version of the code not using LINQ, in case that is not allowed for your homework.
if (randomStepNumber == 0)
{
GameObject validGameObject = null;
while(validGameObject == null)
{
validGameObject = snakeGameObjectFactory.Generate();
foreach(var snake in snakeElements)
{
if (validGameObject.XPosition == snake.XPosition &&
validGameObject.YPosition == snake.YPosition)
{
validGameObject = null;
break;
}
}
if (validGameObject != null)
{
foreach(var gameObject in gameObjects)
{
if (validGameObject.XPosition == gameObject.XPosition &&
validGameObject.YPosition == gameObject.YPosition)
{
validGameObject = null;
break;
}
}
}
}
gameObjects.Add(validGameObject);
randomStepNumber = random.Next(10, 30);
}
I think you may not understand how "continue" works.
if (noCollision > snakeElements.Count + gameObjects.Count)
{
continue;
}
you have that at the beginning of a while loop. If it's found true you will be stuck in an infinite loop.
secondly, you have a While(true) in there that will never let you save a valid result since it's outside the loop. You should replace the while with a do while and check for a valid result at the end of it, then loop if there was a collision.

How can I make GameObjects translate across the screen smoothly in Unity?

Basically, I have a game where tickets appear at the top of the screen. When a ticket is completed, it gets destroyed, and if there's a gap between the remaining tickets, the tickets slide down to fill in the gap. I actually have it working just fine, but they kind of just jump to the positions and it doesn't look very pretty.
How can I move the objects to their target position smoothly? I've tried messing with Lerp and Smoothdamp, but both of those seem to be producing strange results (tickets go all over the place. Here's the relevant code I'm working with:
public void SlideRail()
{
ticketList.RemoveAll(x => x == null);
int count = ticketList.Count;
for (int i = destroyedIndex; i < ticketList.Count; i++)
{
if (ticketList[i] != null)
{
ticketList[i].transform.Translate(Vector3.left * 35);
ticketList[i].GetComponent<Ticket>().index--;
}
}
indexer = railPosition.IndexOf(ticketList.Last().GetComponent<Ticket>().item_pos);
up = false;
}
DestroyedIndex is the index of the object that was destroyed (so that we only move the objects after it in the list)
Up is a flag that activates SlideRail(), when its set to false the method ends and the next method continues.
SlideRail is then called in the Update() method:
void Update()
{
if (up && ticketList.Count > 2)
SlideRail();
if (Time.time > nextTicket)
{
nextTicket = Time.time + timeBetweenTickets;
StartCoroutine(WaitToPrint());
CreateTicket();
UpdatePosition();
}
}
I feel like I should be able to do this using Translate, but I must be missing something.
With movement like this you should usually include Time.deltaTime (as a factor in translate) to bind the movement to the frame rate. [Edit: Translate this sentence to framerate-independant, that's what I meant.]
Now, to the actual point, it looks like you do the movement in just one frame and thus you get this jump (you always set up to false at the end of SlideRail).
A possible way of doing this is to specify a target position upon trigger (task complete, need to push over) and than keep calling ticketList[i].transform.position = Vector3.MoveTowards(ticketList[i].transform.position, targetPosition, speed * Time.deltaTime); for each object (framed by a "reached target position" check, e.g. using if(Vector3.Distance(ticketList[i].transform.position, targetPosition) > someThreshold). You can add an else to this to directly set the position when in threshold range (make the threshold small so it's not visible, e.g. 0.1f), though you usually only need this for non-linear functions like lerp which slow down at the end and can stall. [Edit: Because of floating point imprecision it's actually mostly always a good idea to use a threshold.]
Using iTween, it can be simplified into a one line call:
iTween.MoveTo(someGameObject, iTween.Hash("y", targetY,
"islocal", true,"easetype", iTween.EaseType.spring,
"time", 0.2f));
I've wrote this piece of code for myself before, maybe it helps you.
using System;
using System.Collections;
using UnityEngine;
public class MoveController : MonoBehaviour
{
public float moveTime;
public AnimationCurve moveSpeedCurve;
public AnimationCurve movePositionCurve;
public void StartMoving(Vector2 destination, Action action = null)
{
StartCoroutine(Move(destination, action));
}
IEnumerator Move(Vector3 destination, Action action = null)
{
float currentTime = 0;
float perc = 0;
Vector3 currentPos = transform.localPosition;
while (perc != 1)
{
currentTime += Time.deltaTime;
if (currentTime > moveTime)
{
currentTime = moveTime;
}
perc = moveSpeedCurve.Evaluate(currentTime / moveTime);
transform.localPosition = Vector2.LerpUnclamped(currentPos, destination, movePositionCurve.Evaluate(perc));
yield return null;
}
if (action != null)
action();
}
}

Why won't Unity SetActive execute within Input.GetButtonDown()?

I'm completely stumped by this one. I have a book in game and when you are looking in its general direction while within a certain distance of it and press the Action button (e or left mouse click), it's supposed to display a UI panel (bookPage). Here's my code for it.
void Update () {
Vector3 direction = player.transform.position - this.transform.position;
angle = Vector3.Angle (direction, player.transform.forward);
distance = direction.magnitude;
if (angle >= 160 && distance <= 2 && !bookPage.activeSelf) {
if(Input.GetButtonDown("Action")) {
bookPage.SetActive (true);
}
}
if (bookPage.activeSelf && Input.GetButtonDown("Action")) {
bookPage.SetActive (false);
}
}
This doesn't work. Specifically, the line to set the page to active doesn't work. If it's open, it will close correctly. If I copy and paste bookPage.SetActive (true);
anywhere within this method besides within if(Input.getButtonDown("Action")){}, it will make the bookPage active. If I put Debug.Log("message"); within that if statement though, it will show up in the console. It's just the one line that sets bookPage to active that isn't working. I have no idea why. I've tried moving it to a different method then calling it and doing the same but using a Coroutine. Neither worked. I also tried using other keys which did work, but I need it to work with the action key. It's also worth noting that the action key works in other usage. I already use it to open doors. Has anyone else run into a problem like this?
When you call SetActive(true), activeSelf will return true for your second condition which will invoke SetActive(false) immediately after.
Change this;
if (angle >= 160 && distance <= 2 && !bookPage.activeSelf)
{
if (Input.GetButtonDown("Action"))
{
bookPage.SetActive(true);
}
}
if (bookPage.activeSelf && Input.GetButtonDown("Action"))
{
bookPage.SetActive(false);
}
To this;
if (angle >= 160 && distance <= 2 && !bookPage.activeSelf)
{
if (Input.GetButtonDown("Action"))
{
bookPage.SetActive(true);
}
}
else if (bookPage.activeSelf && Input.GetButtonDown("Action"))
{
bookPage.SetActive(false);
}

Categories

Resources