I'm writing a simple line of code that is when the player jump into the spike, it will trigger the death animation. I try OnTriggerEnter2D but it not working, maybe because of sorting layer? I also try to change the spike layer to be the same as the player layer but it still not working.
here the code:
using UnityEngine;
using System.Collections;
public class HeroKnight : MonoBehaviour {
[SerializeField] float m_speed = 4.0f;
[SerializeField] float m_jumpForce = 7.5f;
[SerializeField] float m_rollForce = 6.0f;
[SerializeField] bool m_noBlood = false;
[SerializeField] GameObject m_slideDust;
private Animator m_animator;
private Rigidbody2D m_body2d;
private Sensor_HeroKnight m_groundSensor;
private Sensor_HeroKnight m_wallSensorR1;
private Sensor_HeroKnight m_wallSensorR2;
private Sensor_HeroKnight m_wallSensorL1;
private Sensor_HeroKnight m_wallSensorL2;
private bool m_isWallSliding = false;
private bool m_grounded = false;
private bool m_rolling = false;
private int m_facingDirection = 1;
private int m_currentAttack = 0;
private float m_timeSinceAttack = 0.0f;
private float m_delayToIdle = 0.0f;
private float m_rollDuration = 8.0f / 14.0f;
private float m_rollCurrentTime;
// Use this for initialization
void Start ()
{
m_animator = GetComponent<Animator>();
m_body2d = GetComponent<Rigidbody2D>();
m_groundSensor = transform.Find("GroundSensor").GetComponent<Sensor_HeroKnight>();
m_wallSensorR1 = transform.Find("WallSensor_R1").GetComponent<Sensor_HeroKnight>();
m_wallSensorR2 = transform.Find("WallSensor_R2").GetComponent<Sensor_HeroKnight>();
m_wallSensorL1 = transform.Find("WallSensor_L1").GetComponent<Sensor_HeroKnight>();
m_wallSensorL2 = transform.Find("WallSensor_L2").GetComponent<Sensor_HeroKnight>();
}
//Here the Trigger event
void OnTriggerEnter2D(Collider2D col)
{
if (gameObject.tag == "Spike")
{
Debug.Log("OnTriggerSEnter2D");
//Death
m_animator.SetBool("noBlood", m_noBlood);
m_animator.SetTrigger("Death");
}
}
In fact, gameObject.tag checks the player tag itself. You must write the code as follows. Also consider that the CompareTag command is more efficient.
public void Update()
{
void OnTriggerEnter2D(Collider2D col)
{
if (col.CompareTag("Spike"))
{
Debug.Log("OnTriggerSEnter2D");
//Death
m_animator.SetBool("noBlood", m_noBlood);
m_animator.SetTrigger("Death");
}
}
}
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'm just starting to learn c# and got stuck in this exercise.
I need to create a formula to deal damage - armor. The armor is reduced with each hit. If I write
float damage = hp+armor-(physicalDamage-damageStrength)it doesn't damage the enemy. The task says I should change only `float damage = ...' for this code to work. So, the mistake can be only in this line float damage =
[RequireComponent(typeof(Animator))]
public class Goblin : MonoBehaviour, IDamagable {
static private string hitTriggerName = "Hit";
[SerializeField] private float hp = 800; // health
[SerializeField] private float armor = 100; // armor
[SerializeField] private float armorStrength = 5; // armor decrease each time
[SerializeField] private int n = 0; // number of hits
private Animator selfAnimator;
private void Awake() {
selfAnimator = GetComponent<Animator>();
}
public void ApplyDamage(float physicalDamage, float damageStrength) {
float damage = hp+armor-physicalDamage-damageStrength;
if (damage < 0) {
hp += damage;
}
n += 1;
armor -= armorStrength;
selfAnimator.SetTrigger(hitTriggerName);
}
public void onHitAnimationEnd() {
if (hp <= 0) {
Kill();
}
}
private void Kill() {
Destroy(gameObject);
}
}```
***Corersponding part of a "player" object***
```using System;
using UnityEngine;
[RequireComponent(typeof(Animator))]
public class Player : MonoBehaviour {
static private string attackTriggerName = "Attack";
[SerializeField] private float attackCooldown = 0.1f;
[SerializeField] private Goblin aim;
private float timeToNextAttack = 0f;
private Animator selfAnimator;
private void Awake() {
selfAnimator = GetComponent<Animator>();
}
[Serializable] private class Staff {
[SerializeField] private float physicalDamage = 100; // staff damage
[SerializeField] private float damageStrength = 5; // damage reduction each time
public void Use(IDamagable aim) {
if (aim != null) {
aim.ApplyDamage(physicalDamage, damageStrength);
}
}
}
[SerializeField] private Staff weapon;
private void Attack() {
selfAnimator.SetTrigger(attackTriggerName);
weapon.Use(aim);
timeToNextAttack = attackCooldown;
}
private void Update() {
if (Input.GetKeyDown(KeyCode.Space) && timeToNextAttack <= 0) {
Attack();
}
timeToNextAttack -= Time.deltaTime;
}
}```
I may be wrong or didn't understand, but by quickly looking at the code, I'd suggest you declare the damage variable above the Awake function. And give it a 0.0f value by default.
I think you made two unrelated mistakes:
damage sould be only: float damage = armor-physicalDamage
damageStrength should not be a parameter of the method because it should be applied to the staff physicalDamage after the hit, like you did to the armor.
so I started unity literally yesterday meaning this is my second day. I followed a tutorial to make a movement script, but when I continuously press space, I can jump like infinitely. This is my code below, is there any way to fix the infinite jump to just two jumps and then it resets when it hits the ground?
using UnityEngine;
public class PlayerMovement : MonoBehaviour
{
[SerializeField] private float speed;
private Rigidbody2D body;
private void Awake()
{
body = GetComponent<Rigidbody2D>();
}
private void Update()
{
body.velocity = new Vector2(Input.GetAxis("Horizontal") * speed, body.velocity.y);
if (Input.GetKey(KeyCode.Space))
body.velocity = new Vector2(body.velocity.x, speed);
}
}
You can prevent this by adding time delay
using UnityEngine;
public class PlayerMovement : MonoBehaviour {
[SerializeField] private float speed;
[SerializeField] private float jumpTimeDelay = 0.5f; // feel free to increase or decrease
private const float timeDelayConst = jumpTimeDelay;
private bool isGrounded = false;
private Rigidbody2D body;
private void Awake() {
body = GetComponent<Rigidbody2D>();
}
private void Update(){
body.velocity = new Vector2(Input.GetAxis("Horizontal") * speed,
body.velocity.y);
isGrounded = jumpTimeDelay <= 0;
jumpTimeDelay -= Time.time;
if (Input.GetKey(KeyCode.Space) && isGrounded){
body.velocity = new Vector2(body.velocity.x, speed);
jumpTimeDelay = timeDelayConst;
}
}
}
This is easily done by adding a boolean value to check if the player is grounded or not and change the value when player jumps. You also need to add a tag to your ground object so we can check if the player is touching the ground. To add tags to objects please refer to Unity - Manual: https://docs.unity3d.com/530/Documentation/Manual/Tags.html
Just for future these kind of questions are asked a lot so you can pretty much google your question and find similar answers.
public class PlayerMovement : MonoBehaviour
{
[SerializeField] private float speed;
private Rigidbody2D body;
private bool isGrounded = true;
private void Awake()
{
body = GetComponent<Rigidbody2D>();
}
private void Update()
{
body.velocity = new Vector2(Input.GetAxis("Horizontal") * speed, body.velocity.y);
if(Input.GetKey(KeyCode.Space))
{
if (isGrounded)
{
isGrounded = false;
body.velocity = new Vector2(body.velocity.x, speed);
}
}
}
private void OnCollisionEnter2D(Collision2D collision)
{
if (collision.gameObject.tag == "Ground") {
isGrounded = true;
}
}
}
I have a problem with mirror, I'm making a online strategy game and you have buildings in the game, with those buildings you can make soldiers, now the buildings have a particles sim to work while they make more solider, but only the server can see the particles sim of its own buildings and the opponents, any ideas why?
that's the code for building smoke:
#region data
[SerializeField] private ParticleSystem[] Explotion = new ParticleSystem[2];
[SerializeField] private Unit UnitPrefab;
[SerializeField] private Transform position;
[SerializeField] private Health MyHp;
[SerializeField] private Image ProgressBar = null;
[SerializeField] private TMP_Text QuaeText = null;
[SerializeField] private float QDurition = 5f;
[SerializeField] private float SpaceSpawn = 7f;
[SerializeField] private int QMax = 5;
[SerializeField] private float progressImageVelocity = 2;
bool played = false;
[SyncVar(hook = nameof(UpdateQText))] private int QuadeUnits;
[SyncVar] private float Timer = 0f;
private ParticleSystem Smoke = null;
private RTSPlayer player;
private float QProgress;
#endregion
//[ServerCallback]
private void UpdateQText(int OldQ,int NewQ)
{
QuaeText.text = NewQ.ToString();
}
private void Update()
{
if(Smoke == null)
{
Smoke = GetComponentInChildren<ParticleSystem>();
}
if(player == null)
{
player = NetworkClient.connection.identity.GetComponent<RTSPlayer>();
}
if (isServer)
{
ProduceUnits();
}
if (isClient)
{
UpdateTimer();
}
if(QuadeUnits <= 0)
{
QuadeUnits = 0;
Timer = 0;
QProgress = 0;
ProgressBar.fillAmount = 0;
}
}
[Server]
private void ProduceUnits()
{
if(QuadeUnits == 0)
{
try
{
Smoke.Stop();
}
catch
{
}
played = false;
return;
}
try
{
if (!played)
{
print("playing");
Smoke.Play();
played = true;
}
}
catch
{
}
Timer += Time.deltaTime;
if(Timer < QDurition) { return; }
GameObject instance1 = Instantiate(UnitPrefab.gameObject, position.position, position.rotation);
NetworkServer.Spawn(instance1, connectionToClient);
Vector3 SpawnPlace = gameObject.transform.position * SpaceSpawn;
SpawnPlace.y = 0;
Movment ClearSpot = instance1.gameObject.GetComponent<Movment>();
ClearSpot.ServerMoveUnit(SpawnPlace);
instance1.gameObject.GetComponent<HPDispaly>().SetEnacled(false);
QuadeUnits--;
Timer = 0f;
}
This wasn't really useful but I found other code that helped me with this problem, if someone has the same problem here's the solution:
Solution
You need to make a syncVar bool and update the animator using it and not with just regular false/true calls, here's my new code:
#region data
[SyncVar(hook = nameof(HandleBuilding))] private bool IsBuildng = false;
[SyncVar(hook = nameof(HandleExplotion))] private bool IsPlaying = false;
[SerializeField] private ParticleSystem[] Explotion = new ParticleSystem[2];
[SerializeField] private Unit UnitPrefab;
[SerializeField] private Transform position;
[SerializeField] private Health MyHp;
[SerializeField] private Image ProgressBar = null;
[SerializeField] private TMP_Text QuaeText = null;
[SerializeField] private float QDurition = 5f;
[SerializeField] private float SpaceSpawn = 7f;
[SerializeField] private int QMax = 5;
[SerializeField] private float progressImageVelocity = 2;
bool played = false;
[SyncVar(hook = nameof(UpdateQText))] private int QuadeUnits;
[SyncVar] private float Timer = 0f;
[SerializeField] private ParticleSystem Smoke = null;
private RTSPlayer player;
private float QProgress;
#endregion
//[ServerCallback]
private void UpdateQText(int OldQ,int NewQ)
{
QuaeText.text = NewQ.ToString();
}
private void HandleBuilding(bool OldBool,bool NewBool)
{
Smoke.gameObject.SetActive(IsBuildng);
}
private void Update()
{
if(Smoke == null)
{
Smoke = GetComponentInChildren<ParticleSystem>();
}
if(player == null)
{
player = NetworkClient.connection.identity.GetComponent<RTSPlayer>();
}
if (isServer)
{
ProduceUnits();
}
if (isClient)
{
UpdateTimer();
}
if(QuadeUnits <= 0)
{
QuadeUnits = 0;
Timer = 0;
QProgress = 0;
ProgressBar.fillAmount = 0;
}
}
[Server]
private void ProduceUnits()
{
if(QuadeUnits == 0)
{
IsBuildng = false;
played = false;
return;
}
else
{
IsBuildng = true;
}
IsBuildng = true;
Timer += Time.deltaTime;
if(Timer < QDurition) { return; }
GameObject instance1 = Instantiate(UnitPrefab.gameObject, position.position, position.rotation);
NetworkServer.Spawn(instance1, connectionToClient);
Vector3 SpawnPlace = gameObject.transform.position * SpaceSpawn;
SpawnPlace.y = 0;
Movment ClearSpot = instance1.gameObject.GetComponent<Movment>();
ClearSpot.ServerMoveUnit(SpawnPlace);
instance1.gameObject.GetComponent<HPDispaly>().SetEnacled(false);
QuadeUnits--;
Timer = 0f;
}
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.