Object exits portal moving the wrong direction in Unity 2D? - c#

Before I start I'd like to say sorry if this isn't highly professionally written, I've been working on this for hours banging my head against the wall trying to figure this out with no luck, I'm tired and stressed.
I'm trying to get a moving object to enter through 1 portal at a specific point, and come out the other portal at the same point it entered. So if the ball enters the top of the portal, the object will come out at the top of the exit portal, and the if the object enters from the bottom of the portal it will exit the bottom of the other portal. I'm not the best a illustration, but here's what I want to it do:
Here you can see in both images the object enters the blue portal and exits the orange at the point that it entered, so top to top, bottom to bottom.
I've actually gotten this to work fine, but now I need to do it again but this time, one of the portals needs to be horizontal instead of vertical:
So what I've done, is make it so when both a vertical, I leave a bool called "exitIsHorizontal" unchecked (false), and when one of them is on the ceiling on the level, it translates the vertical axis to a horizontal one.
I even sort of got that to work, however it's got a reproducible quark that needs to be fixed. When the object enters the bottom of the portal, it works fine like you'd expect and just like the image above. But when you hit the top of the portal, the object comes out the other side of the portal like you'd expect, but the object begins moving in the opposite direction as see in this image:
Correct exit location, wrong exit direction for some reason. I also need this function to be dynamic so that if say, the object hit the blue portal from the other direction that the exit direction would switch sides as well like this:
Here is my script:
public GameObject otherPortal;
public PortalController otherPortalScript;
private BallController ballController;
public bool exitIsHorizontal = false;
List<PortalController> inUseControllers = new List<PortalController>();
// 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 (inUseControllers.Count == 0)
{
inUseControllers.Add(otherPortalScript);
var offset = other.transform.position - transform.position;
if(exitIsHorizontal)
{
offset.x = offset.y;
offset.y = 0;
}
else
{
offset.x = 0;
}
other.transform.position = otherPortal.transform.position + offset;
}
}
}
void OnTriggerExit2D(Collider2D other)
{
if (other.gameObject.tag == "Ball")
{
inUseControllers.Clear();
}
}
This script is attached to both portals so that both Portals can handle both entering and exiting. And any variable you don't see declared with anything in the script making it essentially null (such as "otherPotal") I declare the in editor.
I'll bet it's something extremely simple that I just keep missing, I just don't know that something is.

So a portal is basically a wormhole. An object that enters will retain its local position and direction.
Functions:
Unity has functions for transforming from world space to local space and vice versa.
Position:
Transform.InverseTransformPoint
Transforms position from world space to local space.
Transform.TransformPoint
Transforms position from local space to world space.
Direction:
For transforming directions you will have to use:
Transform.InverseTransformDirection
Transforms a direction from world space to local space. The opposite
of Transform.TransformDirection.
Transform.TransformDirection
Transforms direction from local space to world space.
Simple Example:
One script that you attach to both portals. It moves objects with the tag "Ball" over to the exitPortal.
public class Portal : MonoBehaviour
{
[SerializeField] Portal exitPortal;
void OnTriggerEnter2D(Collider2D collider)
{
if (collider.CompareTag("Ball"))
{
GameObject ball = collider.gameObject;
Rigidbody2D rigidbody = ball.GetComponent<Rigidbody2D>();
Vector3 inPosition = this.transform.InverseTransformPoint(ball.transform.position);
inPosition.x = -inPosition.x;
Vector3 outPosition = exitPortal.transform.TransformPoint(inPosition);
Vector3 inDirection = this.transform.InverseTransformDirection(rigidbody.velocity);
Vector3 outDirection = exitPortal.transform.TransformDirection(inDirection);
ball.transform.position = outPosition;
rigidbody.velocity = -outDirection;
}
}
}
You get this:
Complex Example:
You need 3 scripts for this to work:
Portal: The thing that touches warpable objects
Warpable: A thing that travels through the portals
Ghost: A mirrored warpable that shows while going through a portal
This is what the ghost looks like:
You will need two additional layers – Portal and Ghost, with the collision matrix set as in the image.
Scripts:
I've added enough comments within the code for you to make sense as to what it's doing.
Portal:
public class Portal : MonoBehaviour
{
[SerializeField] Portal exitPortal;
void OnTriggerEnter2D(Collider2D collider)
{
// When a warpable enters a portal create a ghost
if (collider.TryGetComponent(out Warpable warpable))
{
// Create a ghost only if we haven't already
if (warpable.Ghost == null) warpable.CreateGhost(this, exitPortal);
}
}
void OnTriggerExit2D(Collider2D collider)
{
// When a warpable exist a portal; check if it has a ghost
if (collider.TryGetComponent(out Warpable warpable))
{
// Teleport to the ghost; apply its position, rotation, velocity
if (warpable.Ghost != null)
{
// Create vectors to compare dot product
Vector3 portalToWarpable = warpable.transform.position - this.transform.position;
Vector3 portalDownwards = -this.transform.up;
// If warpable is on the other side of the portal you get a value that's more than zero
float dot = Vector3.Dot(portalDownwards, portalToWarpable);
bool passedThroughPortal = dot >= 0f;
// If we passed through the portal then teleport to the ghost; otherwise just continue
if (passedThroughPortal)
{
warpable.Position = warpable.Ghost.warpable.Position;
warpable.Rotation = warpable.Ghost.warpable.Rotation;
warpable.Velocity = warpable.Ghost.warpable.Velocity;
}
// Destroy the ghost
warpable.DestroyGhost();
}
}
}
void OnDrawGizmos()
{
Gizmos.color = Color.magenta;
Gizmos.DrawRay(this.transform.position, this.transform.up);
}
}
Warpable:
public class Warpable : MonoBehaviour
{
[SerializeField] new Rigidbody2D rigidbody;
public Ghost Ghost { get; private set; }
public void CreateGhost(Portal inPortal, Portal outPortal)
{
// Move the ghost object to the Ghost layer, this is so that ghost can collide with real objects, other ghosts, but not with the portal.
// Ghost/Ghost = TRUE
// Ghost/Default = TRUE
// Ghost/Portal = FALSE
GameObject original = this.gameObject;
GameObject duplicate = GameObject.Instantiate(original);
duplicate.layer = LayerMask.NameToLayer("Ghost");
Physics2D.IgnoreCollision(
original.GetComponent<Collider2D>(),
duplicate.GetComponent<Collider2D>()
);
// Add the ghost component
Ghost = duplicate.AddComponent<Ghost>();
Ghost.observing = original.GetComponent<Warpable>();
Ghost.warpable = duplicate.GetComponent<Warpable>();
Ghost.inPortal = inPortal;
Ghost.outPortal = outPortal;
}
public void DestroyGhost()
{
GameObject.Destroy(Ghost.gameObject);
Ghost = null;
}
public Vector3 Position
{
get { return transform.position; }
set { transform.position = value; }
}
public Quaternion Rotation
{
get { return transform.rotation; }
set { transform.rotation = value; }
}
public Vector3 Velocity
{
get { return rigidbody.velocity; }
set { rigidbody.velocity = value; }
}
}
Ghost:
public class Ghost : MonoBehaviour
{
public Warpable observing;
public Warpable warpable;
public Portal inPortal;
public Portal outPortal;
void FixedUpdate()
{
warpable.Position = OutPosition(observing.Position);
warpable.Rotation = OutRotation(observing.Rotation);
warpable.Velocity = OutDirection(observing.Velocity);
}
Vector3 OutPosition(Vector3 position)
{
Vector3 inPosition = -inPortal.transform.InverseTransformPoint(position);
return outPortal.transform.TransformPoint(inPosition);
}
Quaternion OutRotation(Quaternion rotation)
{
return Quaternion.Inverse(inPortal.transform.rotation) * outPortal.transform.rotation * rotation;
}
Vector3 OutDirection(Vector3 velocity)
{
Vector3 inDirection = -inPortal.transform.InverseTransformDirection(velocity);
return outPortal.transform.TransformDirection(inDirection);
}
void OnDrawGizmos()
{
Gizmos.color = Color.cyan;
Gizmos.DrawWireSphere(warpable.Position, 1f);
Gizmos.DrawLine(warpable.Position, warpable.Position + warpable.Velocity);
}
}
And the final result is this:

Related

Why my bomb impact radius and explosion effect instantiate in diffrent possition than it should?

Im working on bomb system in Unity 3D. I have uploaded some explosion effects from Unity Asset Store and I want implement them into my project. I want to plant bomb with key "K" then wait 3sec to detonate it with explosion effect and give some damage to nearby objects. The problem is that explosion appears in such different position as it should. In my opinion this is Editor problem , the code looks fine. I will give you some screenshoots(https://drive.google.com/file/d/19Yzymch9RdTa-E6RkbvvyfzMWjMJHo52/view?usp=sharing)and my bomb script :
public class BoombScript : MonoBehaviour
{
public GameObject boombEffect;
[SerializeField]
private float radius;
[SerializeField]
private float force;
[SerializeField]
private int explosiveDamage;
public void Explode()
{
Instantiate(boombEffect, transform.position, Quaternion.identity);
Debug.Log("Transform" + transform);
Debug.Log("Position" + transform.position);
Collider[] colliders = Physics.OverlapSphere(transform.position, radius);
foreach(Collider rangedObject in colliders)
{
GateScript Gate = rangedObject.GetComponent<GateScript>();
Rigidbody rb = rangedObject.GetComponent<Rigidbody>();
if(Gate != null)
{
Gate.GateDestroy(explosiveDamage);
}
if(rb != null)
{
rb.AddExplosionForce(force, transform.position, radius);
}
}
}
public IEnumerator WaitForExplode()
{
yield return new WaitForSeconds(3f);
Explode();
}
}
In my opinion this is Editor problem , the code looks fine.
In general I would always doubt such a statement.
I can only guess but if I understand you correctly you rather want to use the position of your object the moment when you start the Coroutine, not the one after 3 seconds:
// instead of using the current "reansform.position" rather
// use the pre-stored position passed in via parameter
public void Explode(Vector3 position)
{
Instantiate(boombEffect, position, Quaternion.identity);
Debug.Log("Transform" + transform);
Debug.Log("Position" + position);
Collider[] colliders = Physics.OverlapSphere(position, radius);
foreach(Collider rangedObject in colliders)
{
GateScript Gate = rangedObject.GetComponent<GateScript>();
Rigidbody rb = rangedObject.GetComponent<Rigidbody>();
if(Gate != null)
{
Gate.GateDestroy(explosiveDamage);
}
if(rb != null)
{
rb.AddExplosionForce(force, position, radius);
}
}
}
// If really needed you could still also have an overload
// e.g. if at some places you actually call this method from the outside
// immediately
public void Explode()
{
Explode(transform.position);
}
public IEnumerator WaitForExplode()
{
// Store the position when the routine is started
var position = transform.position;
yield return new WaitForSeconds(3f);
// Instead of using the current position rather pass in
// the previously stored one of when the routine was started
Explode(position);
}

Adding force to LineRenderer relative to the length of the line?

I've lurked around here for ages and found a lot of help through other people's questions. But after extensive searching, I couldn't find a post to help me find a solution to a problem that I'm having, so I decided to post my own question here so if it gets answered, it might help someone else in a similar fashion!
I'm having some issues with adding force to my linerenderer. The basic idea is that I have an object that I shoot forward by dragging a line in the opposite direction. Now all that is missing is for the shot to have force that is relative to the length of the "dragline", as in how "strong" the object is pulled. I'm having some trouble with that so I thought I'd ask someone smarter and more experienced than me!
Here is my script to draw the dragline:
public class DragIndicatorScript : MonoBehaviour
{
Vector3 startPos;
Vector3 endPos;
Camera camera;
LineRenderer lineRenderer;
Vector3 camOffset = new Vector3(0, 0, 10);
[SerializeField] AnimationCurve animCurve;
// Start is called before the first frame update
void Start()
{
camera = Camera.main;
}
// Update is called once per frame
void Update()
{
if (Input.GetMouseButtonDown(0))
{
if (lineRenderer == null)
{
lineRenderer = gameObject.AddComponent<LineRenderer>();
}
lineRenderer.enabled = true;
lineRenderer.positionCount = 2;
startPos = camera.ScreenToWorldPoint(Input.mousePosition) + camOffset;
lineRenderer.SetPosition(0, startPos);
lineRenderer.useWorldSpace = true;
lineRenderer.widthCurve = animCurve;
lineRenderer.numCapVertices = 10;
lineRenderer.material = new Material(Shader.Find("Hidden/Internal-Colored"));
lineRenderer.startColor = Color.magenta;
lineRenderer.endColor = Color.cyan;
}
if (Input.GetMouseButton(0))
{
endPos = camera.ScreenToWorldPoint(Input.mousePosition) + camOffset;
lineRenderer.SetPosition(1, endPos);
}
if (Input.GetMouseButtonUp(0))
{
lineRenderer.enabled = false;
}
}
}
and here is my script to control the shooting of the object:
public class Shoot : MonoBehaviour
{
Rigidbody2D rigidigidy;
Vector2 startpos;
Vector2 endpos;
[SerializeField] float power = 10f;
void Start()
{
rigidigidy = GetComponent<Rigidbody2D>();
}
void Update()
{
if (Input.GetMouseButtonUp(0))
{
endpos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
Launch();
}
}
void OnMouseDown()
{
startpos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
}
void Launch()
{
Vector2 direction = (startpos - endpos).normalized;
rigidigidy.AddForce(direction * power, ForceMode2D.Impulse);
}
}
Thanks in advance! I have used Google for hours on end and written several lines of code and I feel like I just can't find what I'm looking for.
EXTRA QUESTION 1)
How do I clamp the linerenderer to always be where the pullable object is? I wish to make it so that the player can drag anywhere on the playable area, but the linerenderer always instantiates on the object in question (it is always the same object, as in it is the "player model").
EXTRA QUESTION 2)
I know how to change the material of the linerenderer inside the script, but I haven't really found a proper way to call the material from my assets (I made a material that I would like to use as the dragline), so as a placeholder I am just using:
Material(Shader.Find("Hidden/Internal-Colored"));
lineRenderer.startColor = Color.magenta;
lineRenderer.endColor = Color.cyan;
since I wanted to have something there that is even a little more pleasing to the eye than just the default pink one.
The main question is a bit unclear to me. You already have the startpos-endpos. Wouldn't this already be the vector including the magnitude you are looking for?
I would probably rather combine your two scripts and not let them handle input and positions independently.
I would let your Shoot handle the input and only forward it to the DragIndicatorScript since as the name suggests: It shall only be an indicator and should not be the component deciding the final shoot strength.
So the Shoot should rather calculate the input vector, clamp it and only pass this to the indicator which then can update its end position based on the anchor (see below) and the vector.
EXTRA QUESTION 1:
I would simply remember the offset between the anchor and the startPos. Then you can later calculate back the endPoint by simply taking this offset into account.
For being able to drag anywhere on the screen you should not use OnMouseDown which only works if the mouse is actually over your collider but rather go the way you did in DragIndicatorScript and use GetMouseButtonDown(0).
It sounds like you additionally also want a maximum length for the line and use this maximum length also as maximum shoot power. One argument more as said before why I would let shoot control it all.
EXTRA QUESTION 2:
You can simply reference your material in a slot in the Inspector. In general however I would suggest to rather add the LineRenderer already beforehand and completely set it up. Then you don't even need to do it via code!
So it might look somewhat like
public class Shoot : MonoBehaviour
{
[SerializeField] private DragIndicatorScript _dragIndicator;
[SerializeField] private Rigidbody2D _rigidBody;
[SerializeField] private float power = 10f;
[SerializeField] private Camera _camera;
// This script should clamp the input
[SerializeField] private float maxLength;
private Vector2 inputStartPosition;
private void Awake()
{
if(!_rigidBody) _rigidBody = GetComponent<Rigidbody2D>();
if(!_camera) _camera = Camera.main;
}
private void Update()
{
if (Input.GetMouseButtonDown(0))
{
// store the initial input position
inputStartPosition = camera.ScreenToWorldPoint(Input.mousePosition);
// Enable the indicator and for now set the endpoint also on the anchor since there wasn't any input yet
_dragIndicator.SetIndicator(Vector2.zero);
}
if (Input.GetMouseButton(0))
{
// Get the current position
Vector2 inputCurrentPosition = camera.ScreenToWorldPoint(Input.mousePosition);
// Get the input delta between the current and initial position
var input = inputCurrentPosition - inputStartPosition;
// Now clamp this vector to the max length
input = Vector2.ClampMagnitude(input, maxLength);
_dragIndicator.SetIndicator(input);
}
if (Input.GetMouseButtonUp(0))
{
// Just to be sure recalculate from the current position
Vector2 inputCurrentPosition = camera.ScreenToWorldPoint(Input.mousePosition);
var input = inputCurrentPosition - inputStartPosition;
// Now clamp this to the max length
input = Vector2.ClampMagnitude(input, maxLength);
_dragIndicator.HideIndicator();
// Directly pass in the final input vector, this way you don't need to store it in a field
Launch(input);
}
}
private void Launch(Vector2 input)
{
// Input already contains the direction and magnitude of the user input
_rigidBody.AddForce(-input * power, ForceMode2D.Impulse);
}
}
and the DragIndicatorScript only receives these commands:
public class DragIndicatorScript : MonoBehaviour
{
[SerializeField] private LineRenderer _lineRenderer;
// For the EXTRA QUESTION 1
[SerializeField] private Transform _lineAnchor;
// For EXTRA QUESTION 2
[SerializeField] private Material _lineMaterial;
[SerializeField] private AnimationCurve animCurve;
private void Awake()
{
if(!TryGetComponent<LineRenderer>(out _lineRenderer))
{
_lineRenderer = gameObject.AddComponent<LineRenderer>();
}
// Setup your Line ONCE
_lineRenderer.positionCount = 2;
_lineRenderer.useWorldSpace = true;
_lineRenderer.widthCurve = animCurve;
_lineRenderer.numCapVertices = 10;
_lineRenderer.material = _lineMaterial;
_lineRenderer.startColor = Color.magenta;
_lineRenderer.endColor = Color.cyan;
_lineRenderer.SetPosition(0, _lineAnchor.position);
_lineRenderer.enabled = false;
}
public void HideIndicator()
{
_lineRenderer.enabled = false;
}
public void SetIndicator(Vector3 input)
{
lineRenderer.SetPosition(1, _lineAnchor.position + input);
_lineRenderer.enabled = true;
}
}

(Unity C#) NPC not moving on Grass and other terrain

I have created a NPC that follows the main player. When the player is in a certain range of the NPC, the NPC is supposed to walk, run, and attack based on the distance between the player and the NPC. The NPC has an Animator, box collider, Nav Mesh Agent, Enemy Animator And Enemy Controller Script attached. The settings are as follows,
My problem is that the NPC does not chase the player if there's some sort of grass or ferns on the terrain.
The NPC is set to run all types of terrain using Nav Mesh Agent, moreover the bake settings are like in the image. A video of the issue can be seen here.
The code of the enemy controller (although I doubt that is the issue) is as follows:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
public enum EnemyState
{
PATROL,
CHASE,
ATTACK
}
public class EnemyController : MonoBehaviour
{
private EnemyAnimator enemy_Anim;
private NavMeshAgent navAgent;
private EnemyState enemy_State;
public float walk_Speed = 0.5f;
public float run_Speed = 4f;
public float chase_Distance = 7f;
private float current_Chase_Distance;
public float attack_Distance = 1.8f;
public float chase_After_Attack_Distance = 2f;
public float patrol_Radius_Min = 20f, patrol_Radius_Max = 60f;
public float patrol_For_This_Time = 15f;
private float patrol_Timer;
public float wait_Before_Attack = 2f;
private float attack_Timer;
private Transform target;
public GameObject attack_Point;
//private EnemyAudio enemy_Audio;
void Awake()
{
enemy_Anim = GetComponent<EnemyAnimator>();
navAgent = GetComponent<NavMeshAgent>();
target = GameObject.FindWithTag(Tags.PLAYER_TAG).transform;
// enemy_Audio = GetComponentInChildren<EnemyAudio>();
}
// Use this for initialization
void Start()
{
enemy_State = EnemyState.PATROL;
patrol_Timer = patrol_For_This_Time;
// when the enemy first gets to the player
// attack right away
attack_Timer = wait_Before_Attack;
// memorize the value of chase distance
// so that we can put it back
current_Chase_Distance = chase_Distance;
}
// Update is called once per frame
void Update()
{
if (enemy_State == EnemyState.PATROL)
{
Patrol();
}
if (enemy_State == EnemyState.CHASE)
{
Chase();
}
if (enemy_State == EnemyState.ATTACK)
{
Attack();
}
}
void Patrol()
{
// tell nav agent that he can move
navAgent.isStopped = false;
navAgent.speed = walk_Speed;
// add to the patrol timer
patrol_Timer += Time.deltaTime;
if (patrol_Timer > patrol_For_This_Time)
{
SetNewRandomDestination();
patrol_Timer = 0f;
}
if (navAgent.velocity.sqrMagnitude > 0)
{
enemy_Anim.Walk(true);
}
else
{
enemy_Anim.Walk(false);
}
// test the distance between the player and the enemy
if (Vector3.Distance(transform.position, target.position) <= chase_Distance)
{
enemy_Anim.Walk(false);
enemy_State = EnemyState.CHASE;
// play spotted audio
// enemy_Audio.Play_ScreamSound();
}
} // patrol
void Chase()
{
// enable the agent to move again
navAgent.isStopped = false;
navAgent.speed = run_Speed;
// set the player's position as the destination
// because we are chasing(running towards) the player
navAgent.SetDestination(target.position);
if (navAgent.velocity.sqrMagnitude > 0)
{
enemy_Anim.Run(true);
}
else
{
enemy_Anim.Run(false);
}
// if the distance between enemy and player is less than attack distance
if (Vector3.Distance(transform.position, target.position) <= attack_Distance)
{
// stop the animations
enemy_Anim.Run(false);
enemy_Anim.Walk(false);
enemy_State = EnemyState.ATTACK;
// reset the chase distance to previous
if (chase_Distance != current_Chase_Distance)
{
chase_Distance = current_Chase_Distance;
}
}
else if (Vector3.Distance(transform.position, target.position) > chase_Distance)
{
// player run away from enemy
// stop running
enemy_Anim.Run(false);
enemy_State = EnemyState.PATROL;
// reset the patrol timer so that the function
// can calculate the new patrol destination right away
patrol_Timer = patrol_For_This_Time;
// reset the chase distance to previous
if (chase_Distance != current_Chase_Distance)
{
chase_Distance = current_Chase_Distance;
}
} // else
} // chase
void Attack()
{
navAgent.velocity = Vector3.zero;
navAgent.isStopped = true;
attack_Timer += Time.deltaTime;
if (attack_Timer > wait_Before_Attack)
{
enemy_Anim.Attack();
attack_Timer = 0f;
// play attack sound
// enemy_Audio.Play_AttackSound();
}
if (Vector3.Distance(transform.position, target.position) > attack_Distance + chase_After_Attack_Distance)
{
enemy_State = EnemyState.CHASE;
}
} // attack
void SetNewRandomDestination()
{
float rand_Radius = Random.Range(patrol_Radius_Min, patrol_Radius_Max);
Vector3 randDir = Random.insideUnitSphere * rand_Radius;
randDir += transform.position;
NavMeshHit navHit;
NavMesh.SamplePosition(randDir, out navHit, rand_Radius, -1);
navAgent.SetDestination(navHit.position);
}
void Turn_On_AttackPoint()
{
attack_Point.SetActive(true);
}
void Turn_Off_AttackPoint()
{
if (attack_Point.activeInHierarchy)
{
attack_Point.SetActive(false);
}
}
public EnemyState Enemy_State
{
get; set;
}
} // class
I would appreciate very it if someone could help with such an issue!
EDIT: I forgot to add the grass settings, these are as follows. As you can see there are no colliders.
I am digging some more and apparently the only walkable area is as follows (not entire map), how can I adjust this?
Below is how I did:
Painted the terrain with all kinds of grasses I needed and trees.
Undone all the painted grasses and trees.
Baked the navigation mesh(Navmesh) onto the terrain.
Redone all the painted grasses and trees.
Boom! Work done. :)
Why Do grasses not walkable?
Grasses have been painted like trees hence they are carved during baking process as trees.
Trick:
You have to paint the grasses and all the trees needed, then you have to unpaint all the grasses exceptional for the trees. After that bake the terrain, and undone the process(pressing CTRL + Z several times) until you see the grasses repainted.

Switching between two or more cameras unity3D

I'm making a game in which the player controls two different characters (each one has its own empty object with a camera as child), and switchs one or another by pressing the control key. The thing is, I'm trying to make a little transition between both characters cameras by using another camera, so it doesn't just teleports between one and another but I can't seem to do it. I tried with lerp but I don't know if I got it right, so I read and tried Vector3.MoveTowards but still couldn't do it. This is my code so far (the while is because a last-moment-braindead I had):
public class CameraController : MonoBehaviour
{
public Camera cam1;
public Camera cam2;
public Camera movingCamera;
public bool isCurrentPlayer;
public Transform target1;
public Transform target2;
public float speed = 0.2f;
void FixedUpdate()
{
float step = speed * Time.deltaTime;
if (Input.GetButtonDown("Control"))
{
if (isCurrentPlayer)
{
movingCamera.enabled = true;
cam2.enabled = false;
while (transform.position != target1.position)
{
transform.position = Vector3.MoveTowards(transform.position, target1.position, step);
}
if (transform.position == target1.transform.position)
{
movingCamera.enabled = false;
cam1.enabled = true;
}
isCurrentPlayer = false;
}
else if (!isCurrentPlayer)
{
movingCamera.enabled = true;
cam1.enabled = false;
while (transform.position != target2.position)
{
transform.position = Vector3.MoveTowards(transform.position, target2.position, step);
}
if (transform.position == target2.transform.position)
{
movingCamera.enabled = false;
cam2.enabled = true;
}
isCurrentPlayer = true;
}
}
}
I'm curious about two things. Why did you use FixedUpdate to manage your updates? This isn't physics code. Is there a particular reason you are using multiple cameras? If I may, I propose the following changes.
You can simply make use of the main camera instead of multiple cameras. Additionally, you can increase the number of player objects you can toggle through by using an array of player GameObjects, and by changing the input parameters to left control and right control, you can toggle between next player and previous player to navigate bi-directionally through the array of players.
Here's my example code that implements these changes (tested and works, though improvements can be made.)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// Attached to Main Camera
public class CameraController : MonoBehaviour {
// set manually in inspector
public GameObject[] players;
public float movementSpeed = 1.0f;
public float rotationSpeed = 1.0f;
private int currentPlayer;
private float startTime;
private float distanceToPlayer;
private Vector3 startPosition;
private Quaternion startOrientation;
// Use this for initialization
void Start () {
currentPlayer = 0;
ResetCamera();
}
// Update is called once per frame
void Update () {
float distanceCovered;
float rotationCovered;
float fractionTraveled;
// switch to previous
if (Input.GetButtonDown("left ctrl")) {
if (currentPlayer == 0) currentPlayer = players.Length - 1;
else currentPlayer--;
ResetCamera();
}
// switch to nextPlayer
if (Input.GetButtonDown("right ctrl")) {
if (currentPlayer == players.Length - 1) currentPlayer = 0;
else currentPlayer++;
ResetCamera();
}
// Keep moving camera
if (transform.position != players[currentPlayer].transform.position)
{
distanceCovered = (Time.time - startTime) * movementSpeed;
fractionTraveled = distanceCovered / distanceToPlayer;
rotationCovered = (Time.time - startTime) * rotationSpeed;
// Lerp to player position
transform.position = Vector3.Lerp(
startPosition,
players[currentPlayer].transform.position,
fractionTraveled
);
// match player orientation
transform.rotation = Quaternion.RotateTowards(
transform.rotation,
players[currentPlayer].transform.rotation,
rotationCovered
);
// Stop moving camera
} else {
// Match orientation
if (transform.rotation != players[currentPlayer].transform.rotation)
transform.rotation = players[currentPlayer].transform.rotation;
// Set parent transform to current player
transform.parent = players[currentPlayer].transform;
}
}
void ResetCamera() {
transform.parent = null;
startTime = Time.time;
startPosition = transform.position;
startOrientation = transform.rotation;
distanceToPlayer = Vector3.Distance(
transform.position,
players[currentPlayer].transform.position
);
}
}
Obviously the values would need to be tweaked, and the movement algorithm is pretty basic. Crude but function. You can also add player movement code into the camera, just make sure it points to players[currentPlayer] and it will work for each of your player objects without having to use additional scripts unless there is a reason to do so.
Feel free to use this code. Like I said, it works. However, should you choose to do so, it can easily be modified to function like your original code simply by removing the array and reinstating the individual GameObjects.
transform.position points to the position of CameraController game object. If you want to move movingCamera you probably want to use movingCamera.transform.position. Also keep in mind that in your script the MoveTowards() would only fire when you are pressing your "Control" button.
As memBrain said it would be the best practice to use only one camera for this - visually it would look the same.
The script should look something like this:
// Assuming target1 is player 1 and target2 is player 2
private float snapThreshold = 0.1f;
private Vector3 movingCameraDestination = Vector3.zero;
void FixedUpdate()
{
if(Input.GetButtonDown("Control"))
{
if(isCurrentPlayer)
{
//Set position of transition camera to player 1 and set it's destination to player's 2 position
movingCamera.transform.position = player1.position;
movingCameraDestination = player2.position;
//Disable player 1 camera and enable transition camera
cam1.enabled = false;
movingCamera.enabled = true;
}
else
{
//Set position of transition camera to player 21 and set it's destination to player's 1 position
movingCamera.transform.position = player2.position;
movingCameraDestination = player1.position;
//Disable player 1 camera and enable transition camera
cam2.enabled = false;
movingCamera.enabled = true;
}
}
//If transition camera is enabled and its destination is not Vector3.zero - move it
if(movingCameraDestination != Vector3.zero && movingCamera.enabled)
{
movingCamera.transform.position = Vector3.Lerp(movingCamera.transform.position, movingCameraDestination, speed * Time.deltaTime);
//If the distance between transition camera and it's destination is smaller or equal to threshold - snap it to destination position
if(Vector3.Distance(movingCamera.transform.position, movingCameraDestination) <= snapThreshold)
{
movingCamera.transform.position = movingCameraDestination;
}
//If transition camera reached it's destination set it's destination to Vector3.zero and disable it
if(movingCamera.transform.position == movingCameraDestination)
{
movingCameraDestination = Vector3.zero;
movingCamera.enabled = false;
}
}
}

Unity C# Make two Sprites move to each other

So I'm new to Unity, but creating a simple 2D game. I need to make two sprites move to each other when clicking on them. I want to use a script attached to the Main Camera but open for other suggestions. Thanks friends!
Here is my script:
public class MoveTo : MonoBehaviour {
GameObject objectA = null;
GameObject objectB = null;
// Use this for initialization
void Start () {
}
// Update is called once per frame
void Update () {
if(Input.GetMouseButtonDown(0))
{
Ray rayOrigin = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hitInfo;
if(Physics.Raycast(rayOrigin, out hitInfo))
{
//Destroy (hitInfo.collider.gameObject);
if(objectA == null)
{
objectA = hitInfo.collider.gameObject;
}
else{
objectB = hitInfo.collider.gameObject;
}
if(objectA != null && objectB != null)
{
//Need something to make them move towards each other here???
objectA = null;
objectB = null;
}
}
}
}
}
In my opinion moving other gameObjects with a script attached to the camera is breaking the basic idea of components in Unity. It would also be very hard to do if you want the objects to move smoothly and also be able to have multiple pairs of objects moving at same time. You would need some kind of list of moving objects and their destinations. Then you would need to go through the list in the update function of the camera script and update all positions of sprite gameObjects.
I think it is better to attach simple inbetweening script like below to the gameObjects of the sprites.
using UnityEngine;
using System.Collections;
public class Tweener : MonoBehaviour {
public float tweenStopDistance = 0.2f;
public float tweenSpeed = 2.0f;
public Vector3 targetPosition = new Vector3();
void Start () {
targetPosition = transform.position;
}
void Update () {
if((transform.position - targetPosition).magnitude > tweenStopDistance){
transform.position += tweenSpeed * (targetPosition - transform.position).normalized * Time.deltaTime;
}
}
}
In this case you just need to calculate the target position in your camera script.
if(objectA != null && objectB != null)
{
// Calculate position in the middle
Vector3 target = 0.5f * (objectA.transform.position + objectB.transform.position);
// Set middle position as tween target
objectA.GetComponent<Tweener>().targetPosition = target;
objectB.GetComponent<Tweener>().targetPosition = target;
objectA = null;
objectB = null;
}
If you have lot of those sprites and not much computation power on your target machine. You could also attach that script component at run time with gameObject.AddComponent for those sprites that need it. Attaching is probably quite heavy but you wouldn't need to test for already being in the target for sprites that are not moving.

Categories