I'm trying to make a 2D portal in Unity that doesn't just transport the player to the other portal gameobject. But keeps the players position and movements direction and velocity after going through the portal.
I'm be no means an artist, but for example:
The player is a ball bouncing around an area, when he does through the portal his velocity is maintained, along with the fact that he entered he center of the portal.
Where here for example:
If the player enters the bottom half of the portal, he will come out the bottom half of the portal.
When this works, it works great! However, it only works 50% of the time, that 50% can have a bunch of different issues though, sometimes the ball will just not teleport. Sometimes the ball hits the first portal, teleports to the second portal, then back to the first portal, and it does this repeatedly forever. And it experiences these issues seemingly at random.
Here is my Script:
public GameObject otherPortal;
public PortalController otherPortalScript;
private BallController ballController;
public float waitTime = 0.5f;
[HideInInspector]
public bool teleporting;
// Use this for initialization
void Start ()
{
}
// Update is called once per frame
void Update ()
{
}
void OnTriggerEnter2D(Collider2D other)
{
if (other.gameObject.tag == "Ball")
{
ballController = other.GetComponent<BallController>();
if(!teleporting)
{
var offset = other.transform.position - transform.position;
offset.x = 0;
other.transform.position = otherPortal.transform.position + offset;
otherPortalScript.teleporting = true;
teleporting = true;
StartCoroutine("Teleport");
}
}
}
void OnTriggerExit2D(Collider2D other)
{
if (other.gameObject.tag == "Ball")
{
teleporting = false;
otherPortalScript.teleporting = false;
}
}
IEnumerator Teleport()
{
yield return new WaitForSeconds(waitTime);
teleporting = false;
otherPortalScript.teleporting = false;
ballController.teleporting = false;
}
}
The script is attached to both portals, which are both prefabs of the same object. I set both "otherPortal", "otherPortalScript", & "waitTime" in the editor. "waitTime is something I had" to add after the fact to fix another issue I was having where sometimes "teleporting" never got set to false, I believe the the cause of the that problem is the same cause of this problem, making "waitTime" just a bandage for a larger issue. Also, anytime the Portal Script changes a variable in "ballController" such as "ballController.teleporting = false;", it's only there because the ball is add/removing points from a score system, it doesn't at all affect the ball's movement.
Consider getting rid of the teleporting property of the portals and the ball as well as the waitTime.
Now give the ball a List<PortalController> inUseControllers (note you need to add using System.Collections.Generic). Whenever it collision-enters one portal, check if the list is empty via inUseControllers.Count == 0, and if so, add both involved PortalController's to that list and handle the teleporting movement. When the ball collision-exits a PortalController, remove it from the inUseControllers list; it will thus only be emptied again once the ball leaves every portal zone.
This approach should simplify the code, yet safely protect against accidental back-and-forth circles.
Related
I am making a 2D platform game similar to Megaman X/Zero with unity as my side project, and I am trying to implement the dash function.
Following a course on Udemy, now I can make the Player jump,run and shoot bullets, but I can't make it dash.
I used InputSystem to make moves, here is my code:
void OnJump(InputValue value) //OnJump() is automatically recognized by unity, which will run when the jump button was pressed
{
//if it is pressed and also the capsulecollider of the player is touching Groundlayer(is on ground)
//then add velocity to y so it can jump ONCE
if (value.isPressed && myCapsuleCollider.IsTouchingLayers(LayerMask.GetMask("Ground")))
{
myRigid.velocity += new Vector2(0f,jumpSpeed);
}
}
void OnDash(InputValue value)
{
if (value.isPressed)
{
myRigid.velocity += new Vector2(dashSpeed,0f );
}
}
The way I am thinking is: If I can successfully make the player Jump by adding velocity on y-axis, why can't I make it dash using the same way? just add velocity towards another direction?
Jumping works because the rigid body will take care of gravity and apply it for you, resulting in the character falling down, dashing is different, there is no opposite force to decrease the velocity and therefore your character will only speed up when you try dashing.
The correct implementation should disable input when dashing and to revert the added velocity when it's done.
You can implement that best using a coroutine.
The example below applies a speed boost temporarily, it does not disable the input tho:
private bool isDashing = false;
IEnumerator DashCoroutine(int dashDuration)
{
if(isDashing)
yield break;
isDashing = true;
myRigid.velocity = new Vector(dashSpeed,0);
yield return new WaitForSeconds(dashDuration);
myRigid.velocity = new Vector(0,0);
isDashing = false;
}
In the game I'm making, I clone a Ball object whenever a "split ball" power-up is acquired. Everything works as intended except the TrailRenderer material. The Ball prefab has a default material used for TrailRenderer in the beginning. This, however, changes when the ball hits an object controlled by the player (which is called a "bumper"). The material changes work perfectly on collision. Here is the shortened script for the Ball object:
[NonSerialized]
public TrailRenderer trailRenderer;
[SerializeField]
private Material defaultTrailMaterial;
void Start()
{
trailRenderer = GetComponent<TrailRenderer>();
trailRenderer.material = defaultTrailMaterial;
}
private void OnCollisionEnter2D(Collision2D collision)
{
if (CollidedObjectIsABumper(collision))
{
// SetTrailColorToBumperColor() is called along other calculations
}
}
private bool CollidedObjectIsABumper(Collision2D collision)
{
return collision.gameObject.CompareTag("Bumper");
}
private void SetTrailColorToBumperColor()
{
trailRenderer.material = lastCollidedBumper.material;
}
I discarded a few things for clarity.
Now, here is the shortened script for the split ball power-up:
void OnTriggerEnter2D(Collider2D collision)
{
if (!CollidedWithABall(collision))
return;
Ball mainBall = collision.gameObject.GetComponent<Ball>();
// I added this part as a desperate attempt, didn't work.
mainBall.trailRenderer.material = mainBall.lastCollidedBumper.material;
Ball splitBall = Instantiate(mainBall, pos, Quaternion.identity);
splitBall.tag = "Split Ball";
splitBall.GetComponent<TrailRenderer>().material = mainBall.lastCollidedBumper.material;
Destroy(gameObject);
}
private bool CollidedWithABall(Collider2D collision)
{
return collision.gameObject.CompareTag("Ball") || collision.gameObject.CompareTag("Split Ball");
}
pos is a Vector3 variable that is declared in the cut portion. After getting the power-up, this is how the game scene looks like:
None of the balls touched a bumper after getting the power-up. I expect the split ball to have a red trail but it doesn't. I'm sure I'm missing something with Instantiate() but I don't know what.
One thing I assumed was that Instantiate() used the prefab of the main ball, in which case the trail would have a neutral color, but I added an assignment statement after Instantiate() so I don't think that's the only problem here. For reference, the split ball DOES change its trail color when it hits a bumper.
Thank you and please let me know if you need additional information.
As mentioned this is a timing issue.
You have
void Start()
{
trailRenderer = GetComponent<TrailRenderer>();
trailRenderer.material = defaultTrailMaterial;
}
which overwrites your material.
The call of Start on new instantiated objects is delayed until the beginning the next frame for the purpose of being able to still change some field values right after Instantiate before Start is called.
So you set the material in
splitBall.GetComponent<TrailRenderer>().material = mainBall.lastCollidedBumper.material;
But then it is later changed again by Start.
Awake however is called right away.
So either you change it to
void Awake()
{
trailRenderer = GetComponent<TrailRenderer>();
trailRenderer.material = defaultTrailMaterial;
}
so this is done first and then the line
splitBall.GetComponent<TrailRenderer>().material = mainBall.lastCollidedBumper.material;
can correctly overwrite the material or alternatively you could also make
public Material defaultTrailMaterial;
and instead of directly setting the material in
splitBall.GetComponent<TrailRenderer>().material = mainBall.lastCollidedBumper.material;
you rather only set
splitBall.GetComponent<YourComponent>().defaultTrailMaterial = mainBall.lastCollidedBumper.material;
and then let Start do the job as currently.
I'm very new to C# and Unity and am working on a simple platformer for a school project. I'm currently trying to get a piece of group that does damage over time to the player. To detect whether the player is on this piece of ground, I thought I could use the same code as I did for my ground check, but with altered variable names etc. However the ground check code works, and the detection for the damage over time doesn't and I have no idea why.
void CheckIfGrounded()
{
Collider2D collider = Physics2D.OverlapCircle(isGroundedChecker.position, checkGroundRadius, groundLayer);
if (collider != null)
{
isGrounded = true;
}
else
{
if (isGrounded)
{
lastTimeGrounded = Time.time;
}
isGrounded = false;
}
}
void CheckIfDOT()
{
Collider2D collider = Physics2D.OverlapCircle(isDOT.position, checkDOT_AreaRadius, DOT_AreaLayer);
if (collider != null)
{
damageOverTime = true;
}
else
{
damageOverTime = false;
}
}
The first thing is that you need to know how Physics2D.OverlapCircle works.
As in unity documentation, it says :
Checks if a Collider falls within a circular area.
The circle is defined by its centre coordinate in world space and by its radius.
It means that from one point, which is position, they will draw a circle have radius like the radius you use and if there is any collider is in that range, it will result in the collider2d, like your example.
You need to check :
Where is that point : In the player or in the obstacles ?
Is the circle big enough for collider to fall within ? ( depends on radius )
Try to use Debug.Log() to show if the function is executed normally
I'm trying to recreate portal scene using
this video. The link to the scene assets and scripts is under the video.
I'm facing a problem with teleportation:
The player should walk through a plane-trigger in the portal to be moved to the location of the second portal. But it seems to me that after teleportation, he appears in the middle of plane-trigger in the second portal. And if he continues moving, he jumps back to the first portal, and then again to the second portal and so on.
Why is it happening and how can it be fixed? As I understand the author of the script, it should teleport player after passing through the trigger, but the script doesn't work like that for me.
Here is the script for teleportation:
using UnityEngine;
using System.Collections;
public class Sender : MonoBehaviour {
public GameObject player;
public GameObject receiver;
private float prevDot = 0;
private bool playerOverlapping = false;
void Start () {
}
void Update()
{
if (playerOverlapping) {
var currentDot = Vector3.Dot(transform.up, player.transform.position - transform.position);
if (currentDot < 10) // only transport the player once he's moved across plane
{
// transport him to the equivalent position in the other portal
float rotDiff = -Quaternion.Angle(transform.rotation, receiver.transform.rotation);
rotDiff += 180;
player.transform.Rotate(Vector3.up, rotDiff);
Vector3 positionOffset = player.transform.position - transform.position;
positionOffset = Quaternion.Euler(0, rotDiff, 0) * positionOffset;
var newPosition = receiver.transform.position + positionOffset;
player.transform.position = newPosition;
playerOverlapping = false;
}
prevDot = currentDot;
}
}
void OnTriggerEnter(Collider other)
{
if (other.tag == "Player")
{
playerOverlapping = true;
}
}
void OnTriggerExit(Collider other)
{
if (other.tag == "Player")
{
playerOverlapping = false;
}
}
}
One way you could do this would be to simply increase the offset of the player's position when teleported, but then you won't achieve the desired smooth transition.
The other method I thought of would be to add a public boolean variable to the Sender script (I'm assuming that the Sender script is on all portals), with a default value of false.
When you teleport, you set the player's position to the new position using the receiving teleporter's position. Because you have access to the receiving teleporter GameObject, you could use GetComponent to get it's Sender script. You could then set this boolean variable to true before teleporting the player.
Once the player is teleported, the OnTriggerEnter method is automatically invoked, which is your problem. You could put an if statement in OnTriggerEnter so that when it was activated, it would only teleport IF the boolean variable was false. Because it's true when the player teleports, you aren't allowed to teleport back until you leave the portal trigger, then you could set the boolean variable back to false. This would mean that the normal behavior of walking through a portal is unaffected, because the default boolean value is false.
Also, this code would probably be much better if you had your actual teleportation in OnTriggerEnter instead of Update, as Absinthe said.
Hope this helps!
I don't know where you're going with your original code (no pun intended) and I didn't watch through the video (TLDR) but you'll need to rethink this from scratch. Using the Update method is totally inappropriate; update fires on every frame - you want to teleport every frame?
Instead use the OnTriggerEnter method:
void OnTriggerEnter(Collider other)
{
other.transform.position = theVectorYouWantToTeleportTo
}
I am trying to play a particle effect when an enemy is killed but it seems to play on a randomly selected one rather than the one that was hit. However the enemy that was hit still disappears and still add points to the score.
At the moment I have three scripts to carry this out (All have been shortened so I'm only showing the relevant code):
One which is attached to boxes that are thrown at enemies that detects if they have collided with an enemy prefab.
void OnCollisionEnter (Collision theCollision) {
if (canProjectileKill == true) {
// If the projectile hits any game object with the tag "Enemy" or "EnemyContainer".
if (theCollision.gameObject.tag == "Enemy") {
GameObject.Find("EnemyExplosion").GetComponent<enemyDeath>().ProjectileHasHitEnemy();
// Destroy the projectile itself.
Destroy (gameObject);
// Destroy the game object that the projectile has collided with (E.g. the enemy).
Destroy (theCollision.gameObject);
GameObject.Find("Manager").GetComponent<projectileSpawner>().deleteProjectile();
}
}
}
Another that is attached to the enemy prefabs which detects if they have been hit by a box.
void OnCollisionEnter (Collision theCollision) {
if(theCollision.gameObject.tag == "Projectile") {
GameObject.Find("EnemyExplosion").GetComponent<enemyDeath>().EnemyHasBeenHit();
}
}
I then run an if statement asking if both the box has hit the enemy prefab AND if the enemy prefab has been hit by the box in an attempt to identify a single prefab rather than all of them. However this still doesn't work.
public bool HasProjectileHitEnemy;
public bool HasEnemyBeenHitByProjectile;
void Start () {
gameObject.particleSystem.Stop();
HasProjectileHitEnemy = false;
HasEnemyBeenHitByProjectile = false;
}
public void ProjectileHasHitEnemy () {
// From projectile.
HasProjectileHitEnemy = true;
}
public void EnemyHasBeenHit () {
// From enemy.
HasEnemyBeenHitByProjectile = true;
PlayParticleSystem();
}
public void PlayParticleSystem () {
if (HasEnemyBeenHitByProjectile == true && HasProjectileHitEnemy == true) {
gameObject.particleSystem.Play();
HasProjectileHitEnemy = false;
HasEnemyBeenHitByProjectile = false;
}
}
}
I am aware this is a long question but I have been stuck on this for over a week, so any help would be much appreciated. Thank you :)
I'm not sure what kind of object EnemyExplosion is, but your problem seems to be the search for this object. During OnCollisionEnter you know exactly between which objects the collision occurred. But now you're starting a search for any object that is called EnemyExplosion. That's the reason your particles appear random.
Update:
Ok, with your structure something like that
EnemyContainer
- EnemyExplosion
- Particle System
- EnemyModel
- Collider
If EnemyModel contains the collider, you can get to EnemyExplosion and finally enemyDeath the following way.
var explosion = theCollision.transform.parent.gameObject.GetComponent<EnemyExplosion>();
explosion.GetComponent<enemyDeath>().ProjectileHasHitEnemy();
Now that you're accessing the correct object, you can remove some of your double checks and rely on one collider event.
I seem to have found a way around this. Instead I've just set it to instantiate the particle system whenever an enemy detects that it has collided with a projectile. I then use a Coroutine to delete the particle system 2 seconds after.