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.
Related
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.
I have a ScriptableObject called WeaponInfo that keeps track of different information about weapons that doesn't change (magazine max, rate of fire, degradable bool, etc.) and this class also has a reference to a base weapon Prefab that inherits from Monobehaivior. This prefab is what is instantiated and then given a reference to the WeaponInfo scriptable object. My problem with this setup is as follows:
I want the weapon to have a durability value that decreases with each use. I can't put this in the ScriptableObject since ScriptableObjects data is "static", and changing it is not a good idea. I also can't put it inside the GameObject Prefab itself, because every time the player unequips then reequips the weapon, the durability value is reset to its default state.
I looked around the internet for solutions, but no one has a similar example to my setup where the weapon is unequipped by deleting it from the scene. I also want the ability to store this weapon in a chest. Originally I was gonna do this by storing a reference to the ScriptableObject, but I won't be able to save the durability.
What is the best approach to solving this issue? If anyone wants, I can share code, but I wanted to keep the post simple since my issue is conceptual more than it is syntax.
EDIT: Here is the code for my WeaponInfo class:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[CreateAssetMenu(fileName = "New Weapon Item", menuName = "Inventory/Weapon Item")]
public class WeaponItem : Item
{
[Header("Game Object Info")]
public GameObject WeaponPrefab;
public Vector3 DefaultRotation;
public Vector3 DefaultScale;
[SerializeField]
public BulletItem Bullet;
[Header("Weapon Info")]
public bool Automatic;
[SerializeField]
public int Magazine_Max;
[SerializeField]
public int Ammo_Max;
[SerializeField]
public int Reloading_Time;
[SerializeField]
public float ROF;
public bool degradable;
public override void Use()
{
base.Use();
//oldWeapon is of type WeaponItem
var oldWeapon = WeaponSwapper.Instance.SwapWeapon(this);
Inventory.Instance.AddItem(oldWeapon);
Inventory.Instance.RemoveItem(this);
}
}
And here's the code attached to my weapon prefab:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class AmmoWeapon : MonoBehaviour
{
[System.NonSerialized]
public WeaponItem WeaponInfo;
private Inventory inventory;
protected float reloadingTimer;
protected bool reloading;
[SerializeField]
public int Magazine_Current;
[SerializeField]
public int Ammo_Current;
public int durability;
private float elapsedTime = 0;
void Start()
{
inventory = Inventory.Instance;
inventory.OnInventoryChangedCallback += UpdateAmmo;
reloadingTimer = 0;
reloading = false;
LoadAmmo();
}
//this method loads a clip into the magazine
private void LoadAmmo()
{
var totalAmmo = inventory.GetCount(WeaponInfo.Bullet);
if(totalAmmo >= WeaponInfo.Magazine_Max)
{
Magazine_Current = WeaponInfo.Magazine_Max;
Ammo_Current = totalAmmo - WeaponInfo.Magazine_Max;
} else
{
Magazine_Current = totalAmmo;
Ammo_Current = 0;
}
}
//callback for whenever ammo is added to the inventory.. also used to reflect the ammo in the UI
private void UpdateAmmo()
{
var totalAmmo = inventory.GetCount(WeaponInfo.Bullet);
Ammo_Current = totalAmmo;
if (Ammo_Current <= Magazine_Current)
{
Magazine_Current = Ammo_Current;
Ammo_Current = 0;
}
else
Ammo_Current -= Magazine_Current;
}
void Update()
{
//Automatic
if (Input.GetMouseButton(0) && WeaponInfo.Automatic)
{
Fire();
}
//Manual
if (Input.GetMouseButtonDown(0) && !WeaponInfo.Automatic)
{
Fire();
}
//Set reloading flag after a certain amount of time passes
if (reloading)
{
reloadingTimer += Time.deltaTime;
if (reloadingTimer > WeaponInfo.Reloading_Time)
{
reloadingTimer = 0;
reloading = false;
}
}
elapsedTime += Time.deltaTime;
Mathf.Clamp(elapsedTime, 0, WeaponInfo.ROF);
}
protected void Reload()
{
if (reloading)
return;
reloading = true;
reloadingTimer = 0;
LoadAmmo();
}
protected void Fire()
{
if (Magazine_Current == 0)
Reload();
if (!CanFire())
return;
var target = Camera.main.ScreenToWorldPoint(Input.mousePosition);
var bulletPrefab =
Instantiate(WeaponInfo.Bullet.BulletPrefab, transform.position, Quaternion.identity);
var bulletScript = bulletPrefab.GetComponent<Bullet>();
var spriteRenderer = bulletPrefab.GetComponent<SpriteRenderer>();
spriteRenderer.sprite = WeaponInfo.Bullet.Sprite;
bulletScript.BulletInfo = WeaponInfo.Bullet;
bulletScript.SetDirection(target - transform.position);
elapsedTime = 0;
Magazine_Current--;
inventory.RemoveItem(WeaponInfo.Bullet);
//ISSUE IS HERE. durability is reset to initial value whenever this gameobject is initialized again
if (WeaponInfo.degradable)
{
durability--;
if (durability == 0)
{
MessageHandler.Instance.DisplayMessage(WeaponInfo.ItemName + " degraded completely.");
Destroy(gameObject);
}
}
}
protected bool CanFire()
{
if (reloading || elapsedTime < WeaponInfo.ROF || Magazine_Current == 0 || !inventory.Contains(WeaponInfo.Bullet))
return false;
return true;
}
}
And finally, here's the snippet where the weapon is instantiated and attached to my weapon rod:
public WeaponItem SwapWeapon(WeaponItem NewWeapon)
{
DestroyAllChildren();
var previousWeapon = CurrentWeapon;
CurrentWeapon = NewWeapon;
InstantiateAndAttachWeapon(CurrentWeapon);
return previousWeapon;
}
private void InstantiateAndAttachWeapon(WeaponItem weapon)
{
WeaponPrefab = Instantiate(weapon.WeaponPrefab);
var weaponScript = WeaponPrefab.GetComponent<AmmoWeapon>();
weaponScript.WeaponInfo = weapon;
var sprite = WeaponPrefab.GetComponent<SpriteRenderer>();
sprite.sprite = weapon.Sprite;
Debug.Log(CurrentWeapon.DefaultRotation);
WeaponPrefab.transform.parent = transform;
WeaponPrefab.transform.localRotation = Quaternion.Euler(
CurrentWeapon.DefaultRotation);
WeaponPrefab.transform.localScale = weapon.DefaultScale;
WeaponPrefab.transform.localPosition = Vector3.zero;
SetAmmoScript(weaponScript);
}
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.
It's working fine in the Start :
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
public class Waypoints : MonoBehaviour
{
[Header("Objects To Move")]
public Transform objectToMovePrefab;
public int numberOfObjectsToMove = 1;
[Header("Delay At Start")]
public bool useDelay = false;
public bool randomDelayTime = false;
public float delayTime = 3f;
[Header("Movement Speed")]
public float speed = 3f;
public bool randomSpeed = false;
//with this approach, you use GameObjects to represent your waypoints
//(they can be empty if you want the waypoint to be invisible)
[Header("Waypoints")]
[SerializeField] private List<Transform> waypoints;
private List<WaypointsFollower> waypointsFollowers;
[Header("LineRenderer")]
public LineRenderer lineRenderer;
private bool useLineRenderer = true;
private List<Vector3> lineRendererPositions;
[SerializeField] int lineRendererNumOfPositions;
private void Start()
{
for (int i = 0; i < numberOfObjectsToMove; i++)
{
var parent = GameObject.Find("Moving Object Parent");
var objectToMove = Instantiate(objectToMovePrefab, parent.transform);
objectToMove.name = "Platfrom";
}
foreach (GameObject mn in GameObject.FindGameObjectsWithTag("Moving Object"))
{
waypointsFollowers.Add(mn.GetComponent<WaypointsFollower>());
}
StartCoroutine(SendObjectToMove());
}
public int Count => lineRendererPositions.Count;
public Vector3 GetWaypoint(int index)
{
return lineRendererPositions[index];
}
private void Update()
{
if (useLineRenderer && lineRenderer.positionCount > 0 && CurvedLineRenderer.linesSet)
{
lineRendererPositions = GetLinePointsInWorldSpace();
lineRendererNumOfPositions = GetLinePointsInWorldSpace().Count;
foreach(Transform waypoint in waypoints)
{
lineRendererPositions.Add(waypoint.position);
}
useLineRenderer = false;
}
}
private IEnumerator SendObjectToMove()
{
foreach (GameObject mn in GameObject.FindGameObjectsWithTag("Moving Object"))
{
WaypointsFollower waypointsFollower = mn.GetComponent<WaypointsFollower>();
if (useDelay)
{
if (randomDelayTime)
{
yield return new WaitForSeconds(Random.Range(1, 5));
}
else
{
yield return new WaitForSeconds(delayTime);
}
}
if (randomSpeed)
{
waypointsFollower.speed = Random.Range(1, 100);
}
else
{
waypointsFollower.speed = speed;
}
if (waypoints.Count > 0 || lineRendererPositions.Count > 0)
{
waypointsFollower.go = true;
}
else
{
waypointsFollower.go = false;
}
}
}
List<Vector3> GetLinePointsInWorldSpace()
{
var pointsToMove = new Vector3[lineRenderer.positionCount];
//Get the positions which are shown in the inspector
lineRenderer.GetPositions(pointsToMove);
//the points returned are in world space
return pointsToMove.ToList();
}
}
Then on each moving object cloned prefab I added this script :
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class WaypointsFollower : MonoBehaviour
{
[SerializeField] private Waypoints waypoints;
public float speed = 5f;
[SerializeField] private float waypointDistanceThreshold = 0.1f;
[SerializeField] private bool goBack = false;
public bool go = false;
private int waypointIndex = 0;
private void Start()
{
waypoints = GameObject.Find("Waypoints").GetComponent<Waypoints>();
}
void Update()
{
if (go && waypoints.Count > 0)
{
Vector3 waypoint = waypoints.GetWaypoint(waypointIndex);
//movement
float distance = speed * Time.deltaTime;
transform.position = Vector3.MoveTowards(transform.position, waypoint, distance);
//check if we've reached the waypoint
float threshold = waypointDistanceThreshold; //how close is considered having reached the waypoint
if (Vector3.Distance(transform.position, waypoint) < threshold)
{
//wraps back to 0 when we reach last waypoint
if (goBack)
{
waypointIndex = (waypointIndex + 1) % waypoints.Count;
}
else
{
if (waypointIndex != waypoints.Count - 1)
waypointIndex = waypointIndex + 1;
}
}
}
}
}
I can control the speed of each Follower individual but I also want to control the speed of all the Followers from the main Waypoints script and for now I can change the speed in the Inspector and it will affect the changes only in the Start() when starting the game.
How can I make that it will change the speed of all the Followers also in the Update at run time ?
I created a List of all the Followers :
foreach (GameObject mn in GameObject.FindGameObjectsWithTag("Moving Object"))
{
waypointsFollowers.Add(mn.GetComponent<WaypointsFollower>());
}
but not sure how to apply the speed changes in the Update ? Looping in the Update over the Followers each frame is too expensive I guess.
I am not completely sure what you would like to achieve. Well, I know you want to increase the variable Speed on a list of objects that have the component WaypointsFollower(). I suppose what I am confused about is when you want to do this. If Update() is too frequent, what determines when the speed should change?
In your original code, you are using the GameObject.FindGameObjectsWithTag("Moving Object")) too frequently. Only do this once in Awake() or Start(). If any new objects are instantiated, then dynamically add them to the list. Using the FindGameObjectsWithTag frequently can get very expensive. What I am referring to is that you are already storing them, but then in the method SendObjectToMove() you are using it again. Is there a reason you need to call it again in that IEnumartor?
And if you are instantiating these objects on this line
for (int i = 0; i < numberOfObjectsToMove; i++)
{
var parent = GameObject.Find("Moving Object Parent");
var objectToMove = Instantiate(objectToMovePrefab, parent.transform);
objectToMove.name = "Platfrom";
}
Then just store the reference after instantiating them instead of using the FindGameObjectsWithTag.
Here is a very condensed version of your code.
private List<WaypointsFollower> waypointsFollowers;
private void Start()
{
foreach (GameObject mn in GameObject.FindGameObjectsWithTag("Moving Object"))
{
waypointsFollowers.Add(mn.GetComponent<WaypointsFollower>());
}
StartCoroutine(SendObjectToMove());
}
private IEnumerator SendObjectToMove()
{
foreach (WaypointsFollower follwer in waypointsFollowers)
{
// continue your code, but use follower instead of the other object
// you are already storing these objects, so why not reuse them?
}
}
private void UpdateSpeed(float speed)
{
foreach (WaypointsFollower follwer in waypointsFollowers)
{
follower.speed = speed
}
}
I am only using the FindGameObjectsWithTag once in Start() then reusing the list waypointsFollowers it is adding to. I added a function UpdateSpeed that will take a float as a parameter and update every object in the list.
so I was wondering if there was a way to make a spawned object gradually moves/go faster after the player (you the user) collects 10 points. And faster when the player collects another 10 points and so on and so on?
This is my movement script attach to my objects that get spawned in:
public class Movement : MonoBehaviour
{
public static int movespeed = 20;
public Vector3 userDirection = Vector3.right;
public void Update()
{
transform.Translate(userDirection * movespeed * Time.deltaTime);
}
}
This is my score script attach to my player
public int Score;
public Text ScoreText;
void Start ()
{
Score = 0;
SetScoreText ();
}
void OnTriggerEnter2D(Collider2D other)
{
if (other.gameObject.CompareTag ("Pick Up"))
{
other.gameObject.SetActive (false);
Score = Score + 1;
SetScoreText ();
}
}
void SetScoreText ()
{
ScoreText.text = "Score: " + Score.ToString ();
}
And this is my generateEnemy script:
public GameOverManager gameOverManager = null;
[HideInInspector]
public float minBlobSpawnTime = 2;
[HideInInspector]
public float maxBlobSpawnTime = 5;
[HideInInspector]
public bool generateBlobs = true;
[HideInInspector]
public GameObject blobPrefab = null;
[HideInInspector]
public GameObject blobRoot = null;
[HideInInspector]
public float minimumYPosition = -0.425f;
[HideInInspector]
public float maximumYPosition = 0.35f;
[HideInInspector]
public float minDaggerSpawnTime = 2;
[HideInInspector]
public float maxDaggerSpawnTime = 5;
[HideInInspector]
public bool generateDaggers = true;
[HideInInspector]
public GameObject daggerPrefab = null;
[HideInInspector]
public GameObject daggerRoot;
[HideInInspector]
public float minimumXPosition = -11.5f;
[HideInInspector]
public float maximumXPosition = 11.5f;
public Camera camera = null;
// Use this for initialization
void Start ()
{
generateBlobs = ((generateBlobs) && (blobPrefab != null) && (blobRoot != null));
generateDaggers = ((generateDaggers) && (daggerPrefab != null) && (daggerRoot != null));
if (camera == null)
{
Debug.LogError("GenerateEnemy: camera is not set in the inspector. Please set and try again.");
camera = Camera.main;
}
if (gameOverManager == null)
{
Debug.LogError("GenerateEnemy: gameOverManager not set in the inspector. Please set and try again.");
}
if (generateBlobs)
{
StartCoroutine(GenerateRandomEnemy(true, blobPrefab, blobRoot, minBlobSpawnTime, maxBlobSpawnTime));
}
if (generateDaggers)
{
StartCoroutine(GenerateRandomEnemy(false, daggerPrefab, daggerRoot, minDaggerSpawnTime, maxDaggerSpawnTime));
}
}
// Update is called once per frame
void Update ()
{
DestroyOffScreenEnemies();
}
// Spawn an enemy
IEnumerator GenerateRandomEnemy(bool generateOnYAxis, GameObject prefab, GameObject root, float minSpawnTime, float maxSpawnTime)
{
if ((prefab != null) && (gameOverManager != null))
{
if (!gameOverManager.GameIsPaused())
{
GameObject newEnemy = (GameObject) Instantiate(prefab);
newEnemy.transform.SetParent(root.transform, true);
// set this in the prefab instead
// newEnemy.transform.position = new Vector3(newEnemy.transform.parent.position.x, 0.5f, newEnemy.transform.parent.position.z);
// or if you want the y position to be random you need to do something like this
if (generateOnYAxis)
{
newEnemy.transform.position = new Vector3(newEnemy.transform.parent.position.x, Random.Range(minimumYPosition, maximumYPosition), newEnemy.transform.parent.position.z);
}
else
{
newEnemy.transform.position = new Vector3(Random.Range(minimumXPosition, maximumXPosition), newEnemy.transform.parent.position.y, newEnemy.transform.parent.position.z);
}
}
}
yield return new WaitForSeconds(Random.Range(minSpawnTime, maxSpawnTime));
StartCoroutine(GenerateRandomEnemy(generateOnYAxis, prefab, root, minSpawnTime, maxSpawnTime));
}
public void DestroyOffScreenEnemies()
{
GameObject[] enemies = GameObject.FindGameObjectsWithTag("enemy");
if ((enemies.Length > 0) && (camera != null))
{
for (int i = (enemies.Length - 1); i >= 0; i--)
{
// just a precaution
if ((enemies[i] != null) && ((camera.WorldToViewportPoint(enemies[i].transform.position).x > 1.03f) ||
(enemies[i].transform.position.y < -6f)))
{
Destroy(enemies[i]);
}
}
}
}
}
(This script has an generateEnemyCustomEditor script that references it, to show in the inspector)
Thank you :)
First of all, remove the static keyword from public static int movespeed = 20; and use GameObject.Find("ObjectMovementIsAttachedTo").GetComponent<Movement>(); to get the script instance if you want to modify movespeed variable from another script.
And faster when the player collects another 10 points and so on and so
on?
The solution is straight on. Use
if (Score % 10 == 0){
//Increement by number (4) movespeed from Movement script
movement.movespeed += 4;
}
to check if the Score is increased by 10 then increment movespeed by any value you want if that condition is true. It makes sense to put that in the OnTriggerEnter2D function after Score is incremented by 1.
Your new score script:
public class score : MonoBehaviour
{
public int Score;
public Text ScoreText;
private int moveSpeed;
void Start()
{
Score = 0;
SetScoreText();
}
void OnTriggerEnter2D(Collider2D other)
{
if (other.gameObject.CompareTag("Pick Up"))
{
other.gameObject.SetActive(false);
Score = Score + 1;
if (Score % 10 == 0)
{
//Increement by number (4) movespeed from Movement script
moveSpeed += 4;
}
SetScoreText();
}
}
public int getMoveSpeed()
{
return moveSpeed;
}
void SetScoreText()
{
ScoreText.text = "Score: " + Score.ToString();
}
}
Since your Movement script will be instantiated, when you instantiate it, you send its reference to the Player script.
public class Movement : MonoBehaviour
{
public int movespeed = 20;
public Vector3 userDirection = Vector3.right;
score mySpeed;
void Start()
{
//Send Movement instance to the score script
GameObject scoreGameObject = GameObject.Find("GameObjectScoreIsAttachedTo");
mySpeed = scoreGameObject.GetComponent<score>();
}
public void Update()
{
transform.Translate(userDirection * mySpeed.getMoveSpeed() * Time.deltaTime);
}
}