Unity C# - Liskov Substitution Principle - c#

I was rewriting some code to practice SOLID and I think my code is failing the Liskov Substitution Principle. I don't know if this code is correct, can someone help me?
That code was created to control camera rotation, position and orientation for a thir person character. I don't know if that methods need to be separated in other classes or if this code is correct.
My question is related to all the methods of the "AnchorControlForThirdPersonCamera" class, if I must separate them in other classes or not.
Another thing is the use of the "anchor" variable, do I use that variable in "AnchorControlForThirdPersonCamera " class or in "ThirdPersonCamera " an just return when is necessary?
Obs: That class control the anchor but I don't know it is correct if all this methods should be in the same place.
Class:
public class AnchorControlForThirdPersonCamera : MonoBehaviour, IAnchorControlForThirdPersonCamera
{
[Header("Settings")]
[SerializeField] private string anchorName = "CameraAnchor";
[SerializeField] private Vector3 offset = new Vector3(0,1,0);
[Header("Rotation Control")]
[SerializeField] private string inpMouseX = "Mouse X";
[SerializeField] private string inpMouseY = "Mouse Y";
[SerializeField] private float rotationSpeed = 100;
[SerializeField,Range(1,179)] private float minAxisY = 89;
[SerializeField,Range(181,359)] private float maxAxisY = 320;
private Transform anchor;
public Transform GetAnchor() {
return anchor;
}
public void CreateAnchor()
{
if (!anchor)
{
anchor = new GameObject(anchorName).transform;
anchor.hideFlags = HideFlags.HideInHierarchy;
}
}
public void CalculateEulerForAnchorRotation()
{
float axisMouseX = Input.GetAxis(inpMouseX);
float axisMouseY = Input.GetAxis(inpMouseY);
Vector3 anchorEuler = anchor.eulerAngles;
anchorEuler.y += axisMouseX * rotationSpeed * Time.fixedDeltaTime;
anchorEuler.x -= axisMouseY * rotationSpeed * Time.fixedDeltaTime;
if (anchorEuler.x > minAxisY && anchorEuler.x < 180) anchorEuler.x = minAxisY;
if (anchorEuler.x < maxAxisY && anchorEuler.x > 180) anchorEuler.x = maxAxisY;
anchor.eulerAngles = anchorEuler;
}
public void CalculateAnchorPosition(Transform player)
{
anchor.position = player.TransformPoint(offset);
}
public void OnDestroyAnchor()
{
if (anchor)
{
Destroy(anchor);
}
}
}
Interface:
public interface IAnchorControlForThirdPersonCamera
{
Transform GetAnchor();
void CreateAnchor();
/// <summary></summary>
void CalculateEulerForAnchorRotation();
/// <summary>Used in LateUpdate method.</summary>
/// <param name="player"></param>
void CalculateAnchorPosition(Transform player);
void OnDestroyAnchor();
}
Main Code:
public class ThirdPersonCamera : MonoBehaviour
{
private IAnchorControlForThirdPersonCamera _anchorControl;
private ICameraPositionRayForThirdPersonCamera _cameraPositionRay;
private ICameraPositionForThirdPersonCamera _cameraPosition;
private ICameraOrientationForThirdPersonCamera _cameraOrientation;
[SerializeField] private Transform player;
private RaycastHit hitForCameraPos = new RaycastHit();
private Ray rayForCameraPos;
private void Awake()
{
if (GetComponent<ICameraPositionRayForThirdPersonCamera>() == null)
{
gameObject.AddComponent<CameraPositionRayForThirdPersonCamera>();
}
_cameraPositionRay = GetComponent<ICameraPositionRayForThirdPersonCamera>();
if (GetComponent<IAnchorControlForThirdPersonCamera>() == null) {
gameObject.AddComponent<AnchorControlForThirdPersonCamera>();
}
_anchorControl = GetComponent<IAnchorControlForThirdPersonCamera>();
if (GetComponent<ICameraPositionForThirdPersonCamera>() == null)
{
gameObject.AddComponent<CameraPositionForThirdPersonCamera>();
}
_cameraPosition = GetComponent<ICameraPositionForThirdPersonCamera>();
if (GetComponent<ICameraOrientationForThirdPersonCamera>() == null)
{
gameObject.AddComponent<CameraOrientationForThirdPersonCamera>();
}
_cameraOrientation = GetComponent<ICameraOrientationForThirdPersonCamera>();
_anchorControl.CreateAnchor();
}
void Start() {
Cursor.lockState = CursorLockMode.Locked;
Cursor.visible = false;
Transform anchor = _anchorControl.GetAnchor();
_anchorControl.CalculateEulerForAnchorRotation();
rayForCameraPos = _cameraPositionRay.CreateRayForCameraPosition(anchor);
hitForCameraPos = _cameraPosition.SpherecastForCalculateCameraPosition(rayForCameraPos);
_anchorControl.CalculateAnchorPosition(player);
rayForCameraPos = _cameraPositionRay.CreateRayForCameraPosition(anchor);
transform.position = _cameraPosition.GetCameraPosition(rayForCameraPos, hitForCameraPos);
transform.rotation = _cameraOrientation.GetCameraLookAtTargetPoint(anchor, transform);
}
void Update() {
_anchorControl.CreateAnchor();
_anchorControl.CalculateEulerForAnchorRotation();
rayForCameraPos = _cameraPositionRay.CreateRayForCameraPosition(_anchorControl.GetAnchor());
hitForCameraPos = _cameraPosition.SpherecastForCalculateCameraPosition(rayForCameraPos);
}
void LateUpdate()
{
Transform anchor = _anchorControl.GetAnchor();
_anchorControl.CalculateAnchorPosition(player);
rayForCameraPos = _cameraPositionRay.CreateRayForCameraPosition(anchor);
transform.position = _cameraPosition.GetCameraPosition(rayForCameraPos, hitForCameraPos);
transform.rotation = _cameraOrientation.GetCameraLookAtTargetPoint(anchor, transform);
}
private void OnDestroy() {
_anchorControl.OnDestroyAnchor();
}
}
I tryed to implement liskov subistituition principle in this code, but when I tryed to implement in the methods related a anchor control (used to control the position, rotation and orientation of the camera) the code become strange and I don't know if I write correctly.

Related

Problems after adding a stamina system

I've added a stamina system in my game, but when I try do call the function there's a few problems happening:
I can't jump when my character is sprinting and when I jump and press the sprint button my character doesn't fall anymore, he basically just flies.
PlayerController
private Vector3 playerVelocity;
private bool groundedPlayer;
private CharacterController controller;
private PlayerControls playerControls;
private InputManager inputManager;
public HealthBar healthBar;
public StaminaBar staminaBar;
public int currentHealth;
public int maxHealth = 100;
public int currentStamina;
public int maxStamina = 100;
public int staminaDrain = 10;
[SerializeField]
private float playerSpeed = 2.0f;
[SerializeField]
private float playerRunSpeed= 1f;
[SerializeField]
private float jumpHeight = 1.0f;
[SerializeField]
private float gravityValue = -9.81f;
private Transform cameraTransform;
private void Start()
{
currentHealth = maxHealth;
healthBar.SetMaxHealth(maxHealth);
currentStamina = maxStamina;
staminaBar.SetMaxStamina(maxStamina);
controller = GetComponent<CharacterController>();
inputManager = InputManager.Instance;
cameraTransform = Camera.main.transform;
//player = GameObject.Find("Player");
}
void Update()
{
groundedPlayer = controller.isGrounded;
if (groundedPlayer && playerVelocity.y < 0)
{
playerVelocity.y = 0f;
}
Vector2 movement = inputManager.GetPlayerMovement();
Vector3 move = new Vector3(movement.x, 0f, movement.y);
move = cameraTransform.forward * move.z + cameraTransform.right * move.x;
move.y = 0f;
//controller.Move(move * Time.deltaTime * playerSpeed);
if(inputManager.isRunning && currentStamina > 0)
{
controller.Move(move * playerRunSpeed * Time.deltaTime);
staminaBar.UseStamina(staminaDrain);
staminaBar.staminaSlider.value = currentStamina;
}
else
{
controller.Move(move * Time.deltaTime * playerSpeed);
}
// Changes the height position of the player..
if (inputManager.PlayerJumpedThisFrame() && groundedPlayer)
{
playerVelocity.y += Mathf.Sqrt(jumpHeight * -3.0f * gravityValue);
}
playerVelocity.y += gravityValue * Time.deltaTime;
controller.Move(playerVelocity * Time.deltaTime);
}
StaminaBar script
public class StaminaBar : MonoBehaviour
{
public Slider staminaSlider;
private PlayerController playerController;
private WaitForSeconds regenTick = new WaitForSeconds(0.1f);
private Coroutine regen;
public void SetMaxStamina(int stamina){
staminaSlider.maxValue = stamina;
staminaSlider.value = stamina;
}
public void SetStamina(int stamina){
staminaSlider.value = stamina;
}
public void UseStamina(int amount){
if(playerController.currentStamina - amount >= 0){
playerController.currentStamina -= amount;
staminaSlider.value = playerController.currentStamina;
Debug.Log("Losing Stamina");
if(regen != null)
StopCoroutine(regen);
regen = StartCoroutine(RegenStamina());
}
else
{
Debug.Log("NotEnoughStamina");
}
}
private IEnumerator RegenStamina()
{
yield return new WaitForSeconds(2);
while(playerController.currentStamina < playerController.maxStamina){
playerController.currentStamina += playerController.maxStamina/100;
staminaSlider.value = playerController.currentStamina;
yield return regenTick;
}
regen = null;
}
}
Input Manager
{
private StaminaBar staminaBar;
private PlayerController playerController;
[SerializeField]
private float bulletHitMissDistance = 25f;
[SerializeField]
private Transform bulletParent;
[SerializeField]
private Transform barrelTransform;
[SerializeField]
private GameObject bulletPrefab;
[SerializeField]
private float damage = 100;
public float impactForce = 30;
public float fireRate = 8f;
WaitForSeconds rapidFireWait;
public bool isRunning;
private static InputManager _instance;
public static InputManager Instance
{
get {
return _instance;
}
}
private PlayerControls playerControls;
private Transform cameraTransform;
Coroutine fireCoroutine;
private void Awake()
{
if(_instance != null && _instance != this)
{
Destroy(this.gameObject);
}
else
{
_instance = this;
}
playerControls = new PlayerControls();
//Cursor.visible = false;
rapidFireWait = new WaitForSeconds(1/fireRate);
cameraTransform = Camera.main.transform;
playerControls.Player.RunStart.performed += x => Running();
playerControls.Player.RunEnd.performed += x => RunningStop();
playerControls.Player.Shoot.started += _ => StartFiring();
playerControls.Player.Shoot.canceled += _ => StopFiring();
}
private void OnEnable()
{
playerControls.Enable();
//playerControls.Player.Shoot.performed += _ => StartFiring();
}
private void OnDisable()
{
playerControls.Disable();
//playerControls.Player.Shoot.performed += _ => StopFiring();
}
void StartFiring()
{
fireCoroutine = StartCoroutine(RapidFire());
}
void StopFiring()
{
if(fireCoroutine != null)
{
StopCoroutine(fireCoroutine);
}
}
public Vector2 GetPlayerMovement()
{
return playerControls.Player.Movement.ReadValue<Vector2>();
}
public Vector2 GetMouseDelta(){
return playerControls.Player.Look.ReadValue<Vector2>();
}
public bool PlayerJumpedThisFrame(){
return playerControls.Player.Jump.triggered;
}
public void Shooting()
{
RaycastHit hit;
//creates the bullet
GameObject bullet = GameObject.Instantiate(bulletPrefab, barrelTransform.position, Quaternion.identity, bulletParent);
BulletController bulletController = bullet.GetComponent<BulletController>();
//shoots the bullet forwards
if (Physics.Raycast(cameraTransform.position, cameraTransform.forward, out hit, Mathf.Infinity))
{
//checks if the bullet hit something
bulletController.target = hit.point;
bulletController.hit = true;
//makes enemy take damage
Enemy takingDamage = hit.transform.GetComponent<Enemy>();
if (takingDamage != null)
{
takingDamage.TakeDamage(damage);
}
//makes enemy go backwards when hit
if(hit.rigidbody != null)
{
hit.rigidbody.AddForce(-hit.normal * impactForce);
}
}
else
{
bulletController.target = cameraTransform.position + cameraTransform.forward * bulletHitMissDistance;
bulletController.hit = false;
}
}
public IEnumerator RapidFire()
{
while(true)
{
Shooting();
yield return rapidFireWait;
}
}
public void Running()
{
/* if(playerController.currentStamina > 0){
isRunning = true;
staminaBar.UseStamina(playerController.staminaDrain);
staminaBar.staminaSlider.value = playerController.currentStamina;
} */
isRunning = true;
}
public void RunningStop(){
isRunning =false;
}
}
I'm using unity new input system and tried to call the function in two different ways: in the isRunning and when I actually do the sprint function.
I was expecting the player to lose 10 stamina every time I press the sprint button, I was trying to figure that out before trying to make him lose stamina while the button is pressed.
I've seen a couple videos on YouTube, which is where I got the code from, but can't find out what I'm doing wrong when calling the function, I've had similar problems before when trying to call a TakeDamage function but I guess that's a different question.
So here is what I would do.
Instead of controlling the stamina in multiple places and hve forth and back references (=dependencies) between all your scripts I would rather keep this authority within the PlayerController.
Your StaminaBar component should be purely listening and visualizing the current value without having the authority to modify it.
Next step would be to decide for a general code structure
Who is responsible for what?
Who knows / controls what?
There are many possible answers to those but for now an this specific case
You can either say the PlayerController "knows" the StaminaBar just like it also knows the InputManager and can't live without both
Or you could decouple them and let the PlayerController work without having the visualization via the StaminaBar but rather let the StaminaBar listen to the value and just display it .. or not if you want to remove or change this later on
Personally I would go with the second so I will try and give you an example how I would deal with this:
public class PlayerController : MonoBehaviour
{
[Header("Own References")]
[SerializeField] private CharacterController _controller;
[Header("Scene References")]
[SerializeField] private Transform _cameraTransform;
[SerializeField] private InputManager _inputManager;
// In general always make you stuff as encapsulated as possible
// -> nobody should be able to change these except you via the Inspector
// (Values you are anyway not gonna change at all you could also convert to "const")
[Header("Settings")]
[SerializeField] private float _maxHealth = 100f;
[SerializeField] private float _maxStamina = 100f;
[SerializeField] private float _staminaDrainPerSecond = 2f;
[SerializeField] private float _secondsDelayBeforeStaminaRegen = 1f;
[SerializeField] private float _staminaRegenPerSecond = 2f;
[SerializeField] private float _playerSpeed = 1f;
[SerializeField] private float _playerRunSpeed = 2f;
[SerializeField] private float _jumpHeight = 1f;
[SerializeField] private float _gravityValue = -9.81f;
// Your runtime valus
private float _staminaRegenDelayTimer;
private float _currentHealt;
private float _currentStamina;
// You only need a single float for this
private float _currentYVelocity;
// EVENTS we expose so other classes can react to those
public UnityEvent OnDeath;
public UnityEvent<float> OnHealthChanged;
public UnityEvent<float> OnStaminaChanged;
// Provide public read-only access to the settings so your visuals can access those for their setup
public float MaxHealth => _maxHealth;
public float MaxStamina => _maxStamina;
// And then use properties for your runtime values
// whenever you set the value you do additional stuff like cleaning the value and invoke according events
public float currentHealth
{
get => _currentHealt;
private set
{
_currentHealt = Mathf.Clamp(value, 0, _maxHealth);
OnHealthChanged.Invoke(_currentHealt);
if (value <= 0f)
{
OnDeath.Invoke();
}
}
}
public float currentStamina
{
get => _currentStamina;
private set
{
_currentStamina = Mathf.Clamp(value, 0, _maxStamina);
OnStaminaChanged.Invoke(_currentStamina);
}
}
private void Awake()
{
// As a thumb rule to avoid issues with order I usually initialize everything I an in Awake
if (!_controller) _controller = GetComponent<CharacterController>();
currentHealth = MaxHealth;
currentStamina = MaxStamina;
}
private void Start()
{
// in start do the things were you depend on others already being initialized
if (!_inputManager) _inputManager = InputManager.Instance;
if (!_cameraTransform) _cameraTransform = Camera.main.transform;
}
private void Update()
{
UpdateStamina();
UpdateHorizontalMovement();
UpdateVerticalMovement();
}
private void UpdateStamina()
{
if (_inputManager.IsRunning)
{
// drain your stamina -> also informs all listeners
currentStamina -= _staminaDrainPerSecond * Time.deltaTime;
// reset the regen timer
_staminaRegenDelayTimer = _secondsDelayBeforeStaminaRegen;
}
else
{
// only if not pressing run start the regen timer
if (_staminaRegenDelayTimer > 0)
{
_staminaRegenDelayTimer -= Time.deltaTime;
}
else
{
// once timer is finished start regen
currentStamina += _staminaRegenPerSecond * Time.deltaTime;
}
}
}
private void UpdateHorizontalMovement()
{
var movement = _inputManager.PlayerMovement;
var move = _cameraTransform.forward * movement.y + _cameraTransform.right * movement.x;
move.y = 0f;
move *= _inputManager.IsRunning && currentStamina > 0 ? _playerRunSpeed : _playerSpeed;
_controller.Move(move * Time.deltaTime);
}
private void UpdateVerticalMovement()
{
if (_controller.isGrounded)
{
if (_inputManager.JumpedThisFrame)
{
_currentYVelocity += Mathf.Sqrt(_jumpHeight * -3.0f * _gravityValue);
}
else if (_currentYVelocity < 0)
{
_currentYVelocity = 0f;
}
}
else
{
_currentYVelocity += _gravityValue * Time.deltaTime;
}
_controller.Move(Vector3.up * _currentYVelocity * Time.deltaTime);
}
}
And then your StaminaBar shinks down to really only being a display. The PlayerController doesn't care/even know it exists and can fully work without it.
public class StaminaBar : MonoBehaviour
{
[SerializeField] private Slider _staminaSlider;
[SerializeField] private PlayerController _playerController;
private void Awake()
{
// or wherever you get the reference from
if (!_playerController) _playerController = FindObjectOfType<PlayerController>();
// poll the setting from the player
_staminaSlider.maxValue = _playerController.MaxStamina;
// attach a callback to the event
_playerController.OnStaminaChanged.AddListener(OnStaminaChanged);
// just to be sure invoke the callback once immediately with the current value
// so we don't have to wait for the first actual event invocation
OnStaminaChanged(_playerController.currentStamina);
}
private void OnDestroy()
{
if(_playerController) _playerController.OnStaminaChanged.RemoveListener(OnStaminaChanged);
}
// This will now be called whenever the stamina has changed
private void OnStaminaChanged(float stamina)
{
_staminaSlider.value = stamina;
}
}
And just for completeness - I also refactored your InputManager a bit on the fly ^^
public class InputManager : MonoBehaviour
{
[Header("Own references")]
[SerializeField] private Transform _bulletParent;
[SerializeField] private Transform _barrelTransform;
[Header("Scene references")]
[SerializeField] private Transform _cameraTransform;
// By using the correct component right away you can later skip "GetComponent"
[Header("Assets")]
[SerializeField] private BulletController _bulletPrefab;
[Header("Settings")]
[SerializeField] private float _bulletHitMissDistance = 25f;
[SerializeField] private float _damage = 100;
[SerializeField] private float _impactForce = 30;
[SerializeField] private float _fireRate = 8f;
public static InputManager Instance { get; private set; }
// Again I would use properties here
// You don't want anything else to set the "isRunning" flag
// And the others don't need to be methods either
public bool IsRunning { get; private set; }
public Vector2 PlayerMovement => _playerControls.Player.Movement.ReadValue<Vector2>();
public Vector2 MouseDelta => _playerControls.Player.Look.ReadValue<Vector2>();
public bool JumpedThisFrame => _playerControls.Player.Jump.triggered;
private Coroutine _fireCoroutine;
private PlayerControls _playerControls;
private WaitForSeconds _rapidFireWait;
private void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
}
else
{
Instance = this;
}
_playerControls = new PlayerControls();
//Cursor.visible = false;
_rapidFireWait = new WaitForSeconds(1 / _fireRate);
_cameraTransform = Camera.main.transform;
_playerControls.Player.RunStart.performed += _ => Running();
_playerControls.Player.RunEnd.performed += _ => RunningStop();
_playerControls.Player.Shoot.started += _ => StartFiring();
_playerControls.Player.Shoot.canceled += _ => StopFiring();
}
private void OnEnable()
{
_playerControls.Enable();
}
private void OnDisable()
{
_playerControls.Disable();
}
private void StartFiring()
{
_fireCoroutine = StartCoroutine(RapidFire());
}
private void StopFiring()
{
if (_fireCoroutine != null)
{
StopCoroutine(_fireCoroutine);
_fireCoroutine = null;
}
}
private void Shooting()
{
var bulletController = Instantiate(_bulletPrefab, _barrelTransform.position, Quaternion.identity, _bulletParent);
if (Physics.Raycast(_cameraTransform.position, _cameraTransform.forward, out var hit, Mathf.Infinity))
{
bulletController.target = hit.point;
bulletController.hit = true;
if (hit.transform.TryGetComponent<Enemy>(out var enemy))
{
enemy.TakeDamage(_damage);
}
if (hit.rigidbody != null)
{
hit.rigidbody.AddForce(-hit.normal * _impactForce);
}
}
else
{
bulletController.target = _cameraTransform.position + _cameraTransform.forward * _bulletHitMissDistance;
bulletController.hit = false;
}
}
private IEnumerator RapidFire()
{
while (true)
{
Shooting();
yield return _rapidFireWait;
}
}
private void Running()
{
IsRunning = true;
}
private void RunningStop()
{
IsRunning = false;
}
}
You're decreasing and increasing the stamina in the same scope. I think you should let the stamina to be drained when sprint is pressed and start regenerating only if it is released.

Unity C# How to make aiming bool = true, when public void Aim() is active

Here is the code for the weapon
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class WeaponInfo : MonoBehaviour
{
[SerializeField] private Weapon weapon;
[SerializeField] private Transform muzzle;
[Header("Transform")]
public Transform playerCamera;
public Transform DefaultWeaponPos;
public Transform ADSWeaponPos;
AudioSource shootingSound;
[SerializeField] private AudioClip[] pistolClips = default;
private float timeSinceLastShot = 0f;
[HideInInspector] public bool aiming = false;
private void Start()
{
shootingSound = GetComponent<AudioSource>();
Player.shootInput += Shoot;
Player.reloadInput += StartReload;
Player.aimInput += Aim;
aiming = Player.shootInput != null;
}
public void StartReload()
{
if (!weapon.reloading)
StartCoroutine(Reload());
}
private IEnumerator Reload()
{
weapon.reloading = true;
shootingSound.PlayOneShot(pistolClips[2]);
yield return new WaitForSeconds(weapon.reloadTime);
weapon.currentAmmo = weapon.magSize;
weapon.reloading = false;
}
public void Aim()
{
aiming = true;
}
private bool CanShoot() => !weapon.reloading && timeSinceLastShot > 1f / (weapon.fireRate / 60f);
public void Shoot()
{
if(weapon.currentAmmo > 0)
{
if (CanShoot())
{
if (Physics.Raycast(playerCamera.position, playerCamera.forward, out RaycastHit hitInfo, weapon.maxDistance))
{
Debug.DrawLine(playerCamera.transform.position, hitInfo.point, Color.red, 10f);
print(hitInfo.transform.name);
}
shootingSound.PlayOneShot(pistolClips[0]);
weapon.currentAmmo--;
timeSinceLastShot = 0;
OnGunShot();
}
} else if (!weapon.reloading) shootingSound.PlayOneShot(pistolClips[1]);
}
private void Update()
{
timeSinceLastShot += Time.deltaTime;
Debug.DrawRay(playerCamera.position, playerCamera.forward);
transform.position = aiming ? ADSWeaponPos.position : DefaultWeaponPos.position;
}
private void OnGunShot()
{
}
}
Basically what I want is to make it so that when the player is Aiming it changes the weapon position to ADS, but when the player is not aiming it changes it back. I tried to make it so that when aiming = Aim != null but it didnt work, I also tried other methods but I dont know what to do since I tried looking for a solution but didnt find any
And here's the code for invoking the method in the Player Script
if (Input.GetKey(aimKey))
{
aimInput?.Invoke();
}
The aimInput is a public static Action
I figured it out I just need to create another method called sideAimInput in the player, and it makes it set to aiming = false in the weaponInfo script

OnPointerEnter and OnPointerExit not being triggered Unity

Alright so basically the issue that I've been having is that for some reason a GameObject is interfering with the OnPointerEnter function. I'm pretty sure that OnPointerEnter detects only UI. So that's why I'm extremely confused when seeing that a specific GameObject in this case the PlayerLogic GameObject (which you can see in the screenshot) is for some reason interfering with the detection of UI elements. The reason I believe it is this specific GameObject is because once I do PlayerLogic.SetActive(false); OnPointerEnter starts to work again, and I'm also sure that it isn't any of the children of PlayerLogic because I've tried turning them off specifically and it still didn't work.
Inspector of the PlayerLogic object
Hierarchy
The code I'm using to test OnPointerEnter
After some testing I've realized that its the specific issue lies within the Player script on the PlayerLogic GameObject. Now what confuses me is that once I turn off the Player component OnPointer doesn't work, but if I were to remove the Player component completely from the PlayerLogic GameObject OnPointerEnter works.
using UnityEngine;
using UnityEngine.UI;
using TMPro;
using System.Collections;
using System.Collections.Generic;
public class Player : MonoBehaviour, TakeDamage {
[SerializeField] private Animator playerAnimator;
[SerializeField] private Transform mainCameraTransform;
private bool isRunning = false;
[SerializeField] private CharacterController controller;
public float speed = 10f;
[SerializeField] private float jumpForce = 3f;
[SerializeField] private float gravity = -10000.81f;
Vector3 velocity;
Vector3 desiredMoveDirection;
private float dashSpeed = 30f;
private float mouseX;
private float mouseY;
[SerializeField]
private Transform Target;
[SerializeField]
private Transform player;
private float turnSmoothVelocity;
private float time = 0f;
public bool playerIsAttacking = false;
[SerializeField] private Slider playerHealth, playerMana;
[SerializeField] private TextMeshProUGUI healthText, manaText;
private Vector3 originalSpawnPos;
private bool playerIsDead = false;
[SerializeField] private LayerMask enemyLayerMask;
[SerializeField] private Transform playerLook;
private ShowHPBar obj;
private bool HPBarShown = false;
private bool unshowingHPBar = false;
public bool lookingAtEnemy = false;
public RaycastHit hit;
[SerializeField] private Canvas abilityCanvas;
[SerializeField] private Slider CD1;
[SerializeField] private Slider CD2;
[SerializeField] private Slider CD3;
public List<Ability> currentlyEquippedAbilites = new List<Ability>();
public List<string> abilityTexts = new List<string>();
public float[] abilityCooldowns = new float[3];
private float manaRegenTime;
//public List<Image> abilityImages = new List<Image>();
private void Awake() {
Cursor.visible = false;
Cursor.lockState = CursorLockMode.Locked;
}
private void Start() {
playerHealth.onValueChanged.AddListener(delegate {OnValueChangedHealth(); });
playerMana.onValueChanged.AddListener(delegate {OnValueChangedMana(); });
originalSpawnPos = transform.position;
}
private void Update() {
if (!playerIsDead) {
PlayerMovementAndRotation();
}
PlayerDash();
PlayerRun();
PlayerSeeEnemyHealth();
PlayerActivateAbility();
if (manaRegenTime > 0.5f) {
playerMana.value += playerMana.maxValue/100;
manaRegenTime = 0;
}
playerLook.rotation = mainCameraTransform.rotation;
time += Time.deltaTime;
manaRegenTime += Time.deltaTime;
#region Ability Cooldowns
if (currentlyEquippedAbilites.Count > 0) {
if (currentlyEquippedAbilites[0].cooldown <= abilityCooldowns[0])
{
currentlyEquippedAbilites[0].isOnCooldown = false;
abilityCooldowns[0] = 0;
CD1.value = 0;
}
else if (currentlyEquippedAbilites[0].isOnCooldown) {
abilityCooldowns[0] += Time.deltaTime;
CD1.value = currentlyEquippedAbilites[0].cooldown - abilityCooldowns[0];
}
}
if (currentlyEquippedAbilites.Count > 1) {
if (currentlyEquippedAbilites[1].cooldown <= abilityCooldowns[1])
{
currentlyEquippedAbilites[1].isOnCooldown = false;
abilityCooldowns[1] = 0;
CD2.value = 0;
}
else if (currentlyEquippedAbilites[1].isOnCooldown) {
abilityCooldowns[1] += Time.deltaTime;
CD2.value = currentlyEquippedAbilites[1].cooldown - abilityCooldowns[1];
}
}
if (currentlyEquippedAbilites.Count > 2) {
if (currentlyEquippedAbilites[2].cooldown <= abilityCooldowns[2])
{
currentlyEquippedAbilites[2].isOnCooldown = false;
abilityCooldowns[2] = 0;
CD3.value = 0;
}
else if (currentlyEquippedAbilites[2].isOnCooldown) {
abilityCooldowns[2] += Time.deltaTime;
CD3.value = currentlyEquippedAbilites[2].cooldown - abilityCooldowns[2];
}
}
#endregion
}
private void PlayerRun() {
if (Input.GetKey(KeybindsScript.RunKey) && (Input.GetAxisRaw("Horizontal") != 0 || Input.GetAxisRaw("Vertical") != 0)) {
playerAnimator.SetInteger("isRunning", 1);
playerAnimator.SetInteger("isIdle", 0);
playerAnimator.SetInteger("isWalking", 0);
speed = 15f;
}
else if (Input.GetAxisRaw("Horizontal") != 0 || Input.GetAxisRaw("Vertical") != 0) {
playerAnimator.SetInteger("isWalking", 1);
playerAnimator.SetInteger("isIdle", 0);
playerAnimator.SetInteger("isRunning", 0);
speed = 10f;
}
else {
playerAnimator.SetInteger("isRunning", 0);
playerAnimator.SetInteger("isWalking", 0);
playerAnimator.SetInteger("isIdle", 1);
speed = 10f;
}
}
private void PlayerMovementAndRotation() {
bool isGrounded = controller.isGrounded;
if (isGrounded && velocity.y < 0) {
velocity.y = -2f;
}
float horizontal = Input.GetAxisRaw("Horizontal");
float vertical = Input.GetAxisRaw("Vertical");
Vector3 moveDir = (transform.right*horizontal+transform.forward*vertical).normalized;
controller.Move(moveDir*Time.deltaTime*speed);
transform.eulerAngles = new Vector3(0f, mainCameraTransform.eulerAngles.y, 0f);
if (Input.GetKeyDown(KeybindsScript.JumpKey) && isGrounded) {
velocity.y = Mathf.Sqrt(jumpForce * -2f * gravity);
}
velocity.y += gravity * Time.deltaTime;
controller.Move(velocity * Time.deltaTime);
}
private void PlayerDash() {
if (Input.GetKeyDown(KeybindsScript.DashKeybind) && isRunning == false) {
if (Input.GetKey(KeybindsScript.MovementKeyBackward)) {
StartCoroutine(PlayerDashTiming(2));
}
else if (Input.GetKey(KeybindsScript.MovementKeyRight)) {
StartCoroutine(PlayerDashTiming(3));
}
else if (Input.GetKey(KeybindsScript.MovementKeyLeft)) {
StartCoroutine(PlayerDashTiming(4));
}
else {
StartCoroutine(PlayerDashTiming(1));
}
}
}
private void PlayerSeeEnemyHealth() {
if (Physics.Raycast(playerLook.position, playerLook.forward, out hit, 1000f, enemyLayerMask)) {
obj = hit.transform.gameObject.GetComponent<ShowHPBar>();
if (obj != null && !HPBarShown && !unshowingHPBar) {obj.ShowHPBarFunction(); HPBarShown = true;}
lookingAtEnemy = true;
}
else {
if (obj != null && HPBarShown) {StartCoroutine(UnShowHPBar(obj)); HPBarShown = false;}
}
}
public void PlayerEquipAbility(Ability ability, int place) {
if (currentlyEquippedAbilites.Count < place) {currentlyEquippedAbilites.Add(ability);}
else {currentlyEquippedAbilites[place-1] = ability;}
//if (abilityImages.Count < place) {abilityImages.Add(ability.icon);}
//else {abilityImages.Add(ability.icon);}
if (abilityTexts.Count < place) {abilityTexts.Add(ability.name);}
else {abilityTexts[place-1] = ability.name;}
for (int i=0;i < abilityTexts.Count;++i) {
abilityCanvas.transform.GetChild(i).GetChild(0).GetComponent<TextMeshProUGUI>().text = abilityTexts[i];
abilityCanvas.transform.GetChild(i).GetChild(1).GetComponent<Slider>().maxValue = ability.cooldown;
}
}
private void PlayerActivateAbility() {
if (Input.GetKeyDown(KeyCode.Alpha1)) {
if (currentlyEquippedAbilites[0] != null) {
if (currentlyEquippedAbilites[0].manaCost <= playerMana.value && !currentlyEquippedAbilites[0].isOnCooldown) {
ParticleEffect pe = currentlyEquippedAbilites[0].script.gameObject.GetComponent<ParticleEffect>();
playerMana.value -= currentlyEquippedAbilites[0].manaCost;
currentlyEquippedAbilites[0].isOnCooldown = true;
if (pe != null) {pe.PlayAnimation();}
}
}
}
}
public void OnValueChangedHealth() {
healthText.text = playerHealth.value + "/" + PlayerStats.HealthPoints;
}
public void OnValueChangedMana() {
manaText.text = playerMana.value + "/" + PlayerStats.ManaAmount;
}
public void TakeDamageFunction(float damage) {
playerHealth.value -= damage;
if (playerHealth.value <= 0) {
StartCoroutine(PlayerDeath());
}
}
IEnumerator PlayerDashTiming(int x) {
isRunning = true;
float time = 0f;
Vector3 savedVector = Vector3.zero;
switch (x) {
case 1:
savedVector = transform.forward;
break;
case 2:
savedVector = -transform.forward;
break;
case 3:
savedVector = transform.right;
break;
case 4:
savedVector = -transform.right;
break;
}
while(time < .3f)
{
time += Time.deltaTime;
controller.Move(savedVector * dashSpeed * Time.deltaTime);
yield return null;
}
yield return new WaitForSeconds(1.5f);
isRunning = false;
}
IEnumerator PlayerDeath() {
//Respawn
playerIsDead = true;
playerAnimator.enabled = false;
yield return new WaitForSeconds(1f);
playerHealth.value = 100;
transform.position = originalSpawnPos;
yield return new WaitForSeconds(0.1f);
playerIsDead = false;
playerAnimator.enabled = true;
}
IEnumerator UnShowHPBar(ShowHPBar obj) {
unshowingHPBar = true;
yield return new WaitForSeconds(1.5f);
obj.ShowHPBarFunction();
unshowingHPBar = false;
}
}
This is the Player.cs script.
I'm pretty sure that OnPointerEnter detects only UI.
No.
It works "out of the box" on UI because by default every Canvas contains a GraphicsRaycaster component which is then used and handled by the EventSystem.
For non-UI 3D objects you have to make sure
your objects have 3D Colliders
the Colliders are on the same object as the IPointerXY interfaces
on your Camera there is a PhysicsRayster component
For non-UI 2D objects quite similar
your objects have a 2D Collider
the colliders are on the same object as the IPointerXY interfaces
the Camera has a Physics2DRaycaster component
Note that it is possible that any other collider or in general raycast blocking object is between your input and the target object which would also prevent the OnPointerXY to be triggered on your objects.
The CharacterController
is simply a capsule shaped Collider
which can be told to move in some direction from a script.
which is probably blocking the input.
Now with your Player code:
You do
Cursor.lockState = CursorLockMode.Locked;
in Awake so even if you turn it off afterwards this already took effect.

Optimizing sprite change for game object with multiple children

I have an object structure like this:
Which I have to create a player with several different equipment. For this, I am using a custom animation controller which changes an index that would determine sprites out of a few spritesheets:
AnimationController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class AnimationController : MonoBehaviour {
private float waitTime = 0.06f;
private float timer = 0.0f;
private bool grounded;
private bool falling;
private bool jumping;
private bool running;
private bool horizontalCollision;
private HeroMovement heroMovementScript;
private GameObject hero;
private int[] runningSprites = { 2, 3, 4, 5, 6, 7, 8, 13, 14, 15, 16, 17, 18, 19 };
private int animationIndex = 0;
public int currentHeroSpriteIndex;
// Start is called before the first frame update
void Start() {
hero = GameObject.Find("Hero");
heroMovementScript = hero.GetComponent<HeroMovement>();
}
// Update is called based on timer and waitTime
void Update() {
timer += Time.deltaTime;
if (timer > waitTime) {
grounded = heroMovementScript.isGrounded;
falling = heroMovementScript.isFalling;
jumping = heroMovementScript.isJumping;
running = heroMovementScript.isRunning;
horizontalCollision = heroMovementScript.horizontalCollision;
if (running) {
currentHeroSpriteIndex = runningSprites[animationIndex % 14];
animationIndex++;
} else {
currentHeroSpriteIndex = 0;
animationIndex = 0;
}
timer = timer - waitTime;
}
}
}
So here I run the update function a set wait time, and based on variables from a HeroMovement script do I update the booleans to determine how to update the index based on declared arrays:
HeroMovement.cs
using UnityEngine;
public class HeroMovement : MonoBehaviour {
[SerializeField] private float speed;
[SerializeField] private float jumpHeight;
private Rigidbody2D body;
private Animator anim;
private AnimationController animationControllerScript;
private int currentHeroSpriteIndex;
private SpriteRenderer heroRenderer;
private HeroResources heroResourcesScript;
private Sprite[] heroSprites;
public bool isGrounded;
public bool isFalling;
public bool isJumping;
public bool isFacingLeft;
public bool isRunning;
public bool horizontalCollision;
public int collisionCounter = 0;
// called when script is loaded
private void Awake() {
body = GetComponent<Rigidbody2D>();
anim = GetComponent<Animator>();
animationControllerScript = GetComponent<AnimationController>();
heroRenderer = GetComponent<SpriteRenderer>();
heroResourcesScript = GetComponent<HeroResources>();
currentHeroSpriteIndex = animationControllerScript.currentHeroSpriteIndex;
heroSprites = heroResourcesScript.heroSprites;
}
// called on every frame of the game
private void Update() {
float horizontalInput = Input.GetAxis("Horizontal");
float verticalSpeed = body.velocity.y;
// x axis movement
if (!horizontalCollision) {
body.velocity = new Vector2(horizontalInput * speed, body.velocity.y);
// flip player when moving left
if (horizontalInput > 0.01f && isGrounded) {
transform.localScale = Vector3.one;
isFacingLeft = false;
}
// flip player when moving right
else if (horizontalInput < -0.01f && isGrounded) {
transform.localScale = new Vector3(-1, 1, 1);
isFacingLeft = true;
}
}
// jumping
if (Input.GetKey(KeyCode.Space) && isGrounded) {
Jump();
}
isRunning = horizontalInput != 0 && !isJumping && !isFalling;
// set animator parameters
// anim.SetBool("isRunning", horizontalInput != 0 && !isJumping && !isFalling);
// anim.SetBool("isGrounded", isGrounded);
// anim.SetBool("isFalling", isFalling);
// anim.SetBool("isJumping", isJumping);
// anim.SetBool("horizontalCollision", horizontalCollision);
if (!isGrounded && verticalSpeed < -1) {
Fall();
}
}
private void Fall() {
isFalling = true;
}
private void Jump() {
body.velocity = new Vector2(body.velocity.x, jumpHeight);
isJumping = true;
isGrounded = false;
}
private void OnCollisionEnter2D(Collision2D collision) {
Collider2D collider = collision.collider;
Collider2D otherCollider = collision.otherCollider;
if (collision.gameObject.tag == "Ground") {
if (otherCollider.tag == "Hero") {
if (!isHorizontalCollision(otherCollider, collider)) {
isGrounded = true;
isFalling = false;
isJumping = false;
horizontalCollision = false;
} else {
horizontalCollision = true;
if (isBottomCollision(otherCollider, collider)) {
horizontalCollision = false;
}
}
}
}
collisionCounter++;
}
private bool isBottomCollision(Collider2D collider1, Collider2D collider2) {
int c1BottomEdge = (int) collider1.bounds.max.y;
int c2TopEdge = (int) collider2.bounds.min.y;
return c1BottomEdge == c2TopEdge;
}
private bool isHorizontalCollision(Collider2D collider1, Collider2D collider2) {
int c1RightEdge = (int) collider1.bounds.max.x;
int c1LeftEdge = (int) collider1.bounds.min.x;
int c2RightEdge = (int) collider2.bounds.max.x;
int c2LeftEdge = (int) collider2.bounds.min.x;
return (c1RightEdge == c2LeftEdge) || (c1LeftEdge == c2RightEdge);
}
private void OnCollisionExit2D(Collision2D collision) {
collisionCounter--;
if (collisionCounter == 0) {
isGrounded = false;
}
}
// private bool isGrounded() {
// RaycastHit2D raycastHit = Physics2D.BoxCast(boxCollider.bounds.center, boxCollider.bounds.size, 0, Vector2.down, 0.1f, groundLayer);
// return raycastHit.collider != null;
// }
}
Each of the child game objects also make use of the currentHeroSpriteIndex to update their sprites, their color based on serialized fields, and their position based on the parent's:
SpritePosition.cs
using UnityEngine;
public class SpritePosition : MonoBehaviour {
[SerializeField] private string objectName;
[SerializeField] private int objectIndex;
[SerializeField] private int objectR;
[SerializeField] private int objectG;
[SerializeField] private int objectB;
private Rigidbody2D body;
private SpriteRenderer objectRenderer;
private GameObject hero;
private Rigidbody2D heroRigidBody;
private AnimationController animationControllerScript;
private int currentHeroSpriteIndex;
private HeroResources heroResourcesScript;
private Sprite[] spriteGroup;
private void Start() {
body = GetComponent<Rigidbody2D>();
objectRenderer = GetComponent<SpriteRenderer>();
hero = GameObject.Find("Hero");
heroRigidBody = hero.GetComponent<Rigidbody2D>();
animationControllerScript = hero.GetComponent<AnimationController>();
currentHeroSpriteIndex = animationControllerScript.currentHeroSpriteIndex;
heroResourcesScript = hero.GetComponent<HeroResources>();
spriteGroup = heroResourcesScript.spriteGroup[objectName][objectIndex];
float floatR = objectR / 255f;
float floatG = objectG / 255f;
float floatB = objectB / 255f;
if (objectName != "body") {
objectRenderer.color = new Color(floatR, floatG, floatB, 1);
}
}
private void Update() {
SetSprite();
SetPosition();
}
private void SetSprite() {
if (currentHeroSpriteIndex != animationControllerScript.currentHeroSpriteIndex) {
currentHeroSpriteIndex = animationControllerScript.currentHeroSpriteIndex;
objectRenderer.sprite = spriteGroup[currentHeroSpriteIndex];
}
transform.localScale = Vector3.one;
}
// for this to work, the game object must have a
// RigidBody2D component with Freeze Position active
// for X and Y axis
private void SetPosition() {
Vector2 currentHeroPosition = heroRigidBody.position;
transform.position = currentHeroPosition;
}
}
And for this, I use another script, HeroResources which loads all of the sprites on Dictionaries:
HeroResources.cs
using System.Collections.Generic;
using UnityEngine;
public class HeroResources : MonoBehaviour {
const int PANTS_LIMIT = 1;
const int BOOTS_LIMIT = 1;
const int SHIRT_LIMIT = 1;
const int TUNIC_LIMIT = 2;
const int BELT_LIMIT = 1;
public Dictionary<string, Dictionary<int, Sprite[]>> spriteGroup = new Dictionary<string, Dictionary<int, Sprite[]>>();
public Sprite[] heroSprites = new Sprite[180];
public Dictionary<int, Sprite[]> getAllSprites(string name, int limit) {
Dictionary<int, Sprite[]> spriteList = new Dictionary<int, Sprite[]>();
if (name == "hero") {
spriteList.Add(0, Resources.LoadAll<Sprite>("Spritesheets/hero/hero-body"));
} else {
for (int i = 0; i < limit; i++) {
spriteList.Add(i, Resources.LoadAll<Sprite>("Spritesheets/" + name + "/" + (i + 1)));
}
}
return spriteList;
}
void Awake() {
spriteGroup.Add("body", getAllSprites("hero", 1));
spriteGroup.Add("pants", getAllSprites("pants", PANTS_LIMIT));
spriteGroup.Add("boots", getAllSprites("boots", BOOTS_LIMIT));
spriteGroup.Add("shirt", getAllSprites("shirt", SHIRT_LIMIT));
spriteGroup.Add("tunic", getAllSprites("tunic", TUNIC_LIMIT));
spriteGroup.Add("belt", getAllSprites("belt", BELT_LIMIT));
heroSprites = Resources.LoadAll<Sprite>("Spritesheets/hero/hero-body");
}
}
So, to clarify: the Hero game object has the scripts AnimationController, HeroMovement, and HeroResources attached to it, while the child game objects only have the SpritePosition script attached.
The idea is to load the sprites, then based on the logic in the movement script, decide on the booleans to use for the animator, check which is active (currently I only have running working), then based on which one is true determine a sprite index to use. All sprites have the same name and dimensions which is why a single index works to change all. I'm basically doing it this way to avoid having hundreds of animations for each different equipment in use.
So, while the sprites update this way in sync, I'm not sure if I'm not optimizing the sprite changing (and redrawing in color) process, because when I press arrow keys to run, I get the following:
Although the screen recorder I used seems to slow down the playback, there are brief moments in which the hero's body is seen through the pants (and sometimes the boots) although the sprites are supposed to not overlap.
I'm not sure if this is because of a computer's memory, but there are 14 sprites used for running movement, which would amount to 84 sprites in total used in the span of the 2 or so seconds which the animation lasts. Is it that I'm using too much memory to load the sprites needed? Should I maybe try to find a way to merge sprites to only have a single child whose sprite gets updated? Please let me know if anyone has suggestions on how to improve my code's performance.
EDIT:
I opted to change my code to do away with the HeroResources.cs script which ultimately loads every available asset at the beginning to instead include the resource load in SpritePosition.cs so that each child game object takes care of its own spritesheet:
using UnityEngine;
public class SpritePosition : MonoBehaviour {
[SerializeField] private string objectName;
[SerializeField] private int objectIndex;
[SerializeField] private int objectR;
[SerializeField] private int objectG;
[SerializeField] private int objectB;
private Rigidbody2D body;
private SpriteRenderer objectRenderer;
private GameObject hero;
private Rigidbody2D heroRigidBody;
private AnimationController animationControllerScript;
private int currentHeroSpriteIndex;
private Sprite[] spriteGroup;
private void Start() {
body = GetComponent<Rigidbody2D>();
objectRenderer = GetComponent<SpriteRenderer>();
hero = GameObject.Find("Hero");
heroRigidBody = hero.GetComponent<Rigidbody2D>();
animationControllerScript = hero.GetComponent<AnimationController>();
currentHeroSpriteIndex = animationControllerScript.currentHeroSpriteIndex;
if (objectName != "body") {
LoadSpriteSheet();
} else {
spriteGroup = Resources.LoadAll<Sprite>("Spritesheets/hero/hero-body");
}
float floatR = objectR / 255f;
float floatG = objectG / 255f;
float floatB = objectB / 255f;
if (objectName != "body") {
objectRenderer.color = new Color(floatR, floatG, floatB, 1);
}
}
private void Update() {
SetSprite();
SetPosition();
}
private void LoadSpriteSheet() {
spriteGroup = Resources.LoadAll<Sprite>("Spritesheets/" + name + "/" + (objectIndex + 1));
}
private void SetSprite() {
if (currentHeroSpriteIndex != animationControllerScript.currentHeroSpriteIndex) {
currentHeroSpriteIndex = animationControllerScript.currentHeroSpriteIndex;
objectRenderer.sprite = spriteGroup[currentHeroSpriteIndex];
}
transform.localScale = Vector3.one;
}
private void SetPosition() {
Vector2 currentHeroPosition = heroRigidBody.position;
transform.position = currentHeroPosition;
}
}
This, however, doesn't seem to improve performance. I've checked the profiler using deep profiling, and though I see large spikes I don't see garbage coming from the SetSprite function nor much of a delay compared to EditorLoop or PlayerLoop:
I assumed there would be issues in how often the function to change sprites gets called, or how much garbage would be generated with the constant sprite changing, but it seems that this might not be the issue.
If you ever want to discover where lag or high memory usage is coming from use the unity profiler, if possible use a 3rd party profiler because unity's is kinda meh, also use deep profiling (unity will profile every method call, good to see what changes are causing lag, remember that using the profiler will reduce performance and increase memory usage as long as you have it on). There is a lot of stuff that you NEED to improve, but all of the issues I can think off will be obvious with deep profiling.

Unity 2D, control a simple shot

I have to control a shot of a ball with touch. All works fine but I need to start my trajectory when I touch the screen, but the trajectory start only when the ball is touched. Is because the script is attached to the ball?
All touch input except touches on the ball is ignored.
Here is the C# script , can someone help me?
using UnityEngine;
using System.Collections;
using System;
namespace Shorka.BallTrajectory
{
public enum PullState
{
Idle,
UserPulling,
ObjFlying
}
public class PullCtrl : MonoBehaviour
{
#region fields
//[SerializeField] private Transform throwTarget;
[SerializeField] private ThrownObject throwObj;
[SerializeField] private Transform dotHelper;
[SerializeField] private Transform pullingStartPoint;
[Space(5)]
[Tooltip("this linerenderer will draw the projected trajectory of the thrown object")]
[SerializeField]
private LineRenderer trajectoryLineRen;
[SerializeField]
private TrailMaker trail;
[Space(5)]
[SerializeField]
private float throwSpeed = 10F;
[Tooltip("Max Distance between 'PullingStartPoint' and pulling touch point")]
[SerializeField]
private float maxDistance = 1.5F;
[SerializeField]
private float coofDotHelper = 1.5F;
[Tooltip("Related to length of trajectory line")]
[SerializeField]
private int qtyOfsegments = 13;
[Tooltip("Step of changing trajectory dots offset in runtime")]
[SerializeField]
private float stepMatOffset = 0.01F;
[Tooltip("Z position of trajectory dots")]
[SerializeField]
private float dotPosZ = 0F;
private PullState pullState;
private Camera camMain;
//private Collider2D collThrowTarget;
private Rigidbody2D rgThrowTarget;
private Vector3 posPullingStart;
private Vector3 initPos;
private TrajectoryCtrl trajCtrl;
#endregion
public Vector3 PosDotHelper { get { return dotHelper.position; } }
public Vector3 PosThrowTarget { get { return throwObj.transform.position; } }
public int QtyOfsegments { get { return qtyOfsegments; } }
public float DotPosZ { get { return dotPosZ; } }
public Vector3 PosPullingStart { get { return posPullingStart; } }
public float StepMatOffset { get { return stepMatOffset; } }
void Awake()
{
trail.emit = false;
trajCtrl = new TrajectoryCtrl(this, trajectoryLineRen);
}
void Start()
{
camMain = Camera.main;
pullState = PullState.Idle;
posPullingStart = pullingStartPoint.position;
initPos = PosThrowTarget;
}
void Update()
{
SwitchStates();
}
private void SwitchStates()
{
switch (pullState)
{
case PullState.Idle:
if (Input.touchCount> 0 && Input.GetTouch(0).phase == TouchPhase.Began)
{
Debug.Log("Screen touched");
//get the point on screen user has tapped
Vector3 location = camMain.ScreenToWorldPoint(Input.GetTouch(0).position);
//if user has tapped onto the ball
if (throwObj.Collider == Physics2D.OverlapPoint(location))
pullState = PullState.UserPulling;
}
break;
case PullState.UserPulling:
dotHelper.gameObject.SetActive(true);
if (Input.touchCount > 0 && Input.GetTouch(0).phase == TouchPhase.Moved)
{
//get touch position
Vector3 touchPos = camMain.ScreenToWorldPoint(Input.GetTouch(0).position);
touchPos.z = 0;
//we will let the user pull the ball up to a maximum distance
if (Vector3.Distance(touchPos, posPullingStart) > maxDistance)
{
Vector3 maxPosition = (touchPos - posPullingStart).normalized * maxDistance + posPullingStart;
maxPosition.z = dotHelper.position.z;
dotHelper.position = maxPosition;
}
else
{
touchPos.z = dotHelper.position.z;
dotHelper.position = touchPos;
}
float distance = Vector3.Distance(posPullingStart, dotHelper.position);
trajCtrl.DisplayTrajectory(distance);
}
else if (Input.touchCount > 0 && Input.GetTouch(0).phase == TouchPhase.Canceled || Input.GetTouch(0).phase == TouchPhase.Ended)
{
float distance = Vector3.Distance(posPullingStart, dotHelper.position);
trajectoryLineRen.enabled = false;
ThrowObj(distance);
}
break;
default:
break;
}
}
//private Vector2 velocityToRg = Vector2.zero;
private void ThrowObj(float distance)
{
Debug.Log("ThrowObj");
pullState = PullState.Idle;
Vector3 velocity = posPullingStart - dotHelper.position;
//velocityToRg = CalcVelocity(velocity, distance);
throwObj.ThrowObj(CalcVelocity(velocity, distance));
//rgThrowTarget.velocity = velocityToRg;
//rgThrowTarget.isKinematic = false;
trail.enabled = true;
trail.emit = true;
dotHelper.gameObject.SetActive(false);
}
public void Restart(Vector3 posThrownObj)
{
trail.emit = false;
trail.Clear();
StartCoroutine(ClearTrail());
trajectoryLineRen.enabled = false;
dotHelper.gameObject.SetActive(false);
pullState = PullState.Idle;
throwObj.Reset(posThrownObj);
}
private readonly WaitForSeconds wtTimeBeforeClear = new WaitForSeconds(0.3F);
IEnumerator ClearTrail()
{
yield return wtTimeBeforeClear;
trail.Clear();
trail.enabled = false;
}
Vector3 velocity = Vector3.zero;
public Vector3 CalcVelocity(Vector3 diff, float distance)
{
velocity.x = diff.x * throwSpeed * distance * coofDotHelper;
velocity.y = diff.y * throwSpeed * distance * coofDotHelper;
return velocity;
}
}
}
Its quite easy. you need to add these two scripts to the gameobject (not the ball but the one which is used to shoot the ball something like a gun)
TouchEventTrigger.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.EventSystems;
public class TouchEventTrigger : MonoBehaviour, IPointerClickHandler, IPointerDownHandler, IPointerUpHandler {
public TouchEvent onClick;
public TouchEvent onDown;
public TouchEvent onUp;
public void OnPointerClick(PointerEventData e) {
onClick.Invoke(e);
}
public void OnPointerDown(PointerEventData e) {
onDown.Invoke(e);
}
public void OnPointerUp(PointerEventData e) {
onUp.Invoke(e);
}
}
[System.Serializable]
public class TouchEvent : UnityEvent<PointerEventData> {}
Shooter.cs
public void TryBeginAim(PointerEventData e) {
if(canShoot) {
initialTouchPos = e.position;
}
}
public void TryAim(PointerEventData e) {
if(canShoot) {
direction = initialTouchPos - e.position;
float mag = (initialTouchPos - e.position).magnitude;
float scale = Mathf.Clamp(mag/maxMagnitude, scaleThreshold, 1f);
angle = Mathf.Atan2(direction.y,direction.x) * Mathf.Rad2Deg + 90f;
aim.transform.rotation = Quaternion.AngleAxis(angle, Vector3.forward);
aim.transform.localScale = Vector3.one * scale;
aim.SetActive(e.position.y < initialTouchPos.y);
}
}
public void TryShoot(PointerEventData e) {
if(canShoot && aim.activeInHierarchy) {
canShoot = false;
StartCoroutine(ShootRoutine(balls));
}
aim.SetActive(false);
}
private IEnumerator ShootRoutine(int b) {
// write code to start shooting balls.
}
Then connect the touch events with the methods like this:
1. OnPointerClick => TryBeginAim
2. OnPointerDown => TryAim
3. OnPointerUp => TryShoot
That's All !!
I hope this helps to solve this issue. Enjoy!

Categories