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);
}
Related
Here is the code for the weapon
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class WeaponInfo : MonoBehaviour
{
[SerializeField] private Weapon weapon;
[SerializeField] private Transform muzzle;
[Header("Transform")]
public Transform playerCamera;
public Transform DefaultWeaponPos;
public Transform ADSWeaponPos;
AudioSource shootingSound;
[SerializeField] private AudioClip[] pistolClips = default;
private float timeSinceLastShot = 0f;
[HideInInspector] public bool aiming = false;
private void Start()
{
shootingSound = GetComponent<AudioSource>();
Player.shootInput += Shoot;
Player.reloadInput += StartReload;
Player.aimInput += Aim;
aiming = Player.shootInput != null;
}
public void StartReload()
{
if (!weapon.reloading)
StartCoroutine(Reload());
}
private IEnumerator Reload()
{
weapon.reloading = true;
shootingSound.PlayOneShot(pistolClips[2]);
yield return new WaitForSeconds(weapon.reloadTime);
weapon.currentAmmo = weapon.magSize;
weapon.reloading = false;
}
public void Aim()
{
aiming = true;
}
private bool CanShoot() => !weapon.reloading && timeSinceLastShot > 1f / (weapon.fireRate / 60f);
public void Shoot()
{
if(weapon.currentAmmo > 0)
{
if (CanShoot())
{
if (Physics.Raycast(playerCamera.position, playerCamera.forward, out RaycastHit hitInfo, weapon.maxDistance))
{
Debug.DrawLine(playerCamera.transform.position, hitInfo.point, Color.red, 10f);
print(hitInfo.transform.name);
}
shootingSound.PlayOneShot(pistolClips[0]);
weapon.currentAmmo--;
timeSinceLastShot = 0;
OnGunShot();
}
} else if (!weapon.reloading) shootingSound.PlayOneShot(pistolClips[1]);
}
private void Update()
{
timeSinceLastShot += Time.deltaTime;
Debug.DrawRay(playerCamera.position, playerCamera.forward);
transform.position = aiming ? ADSWeaponPos.position : DefaultWeaponPos.position;
}
private void OnGunShot()
{
}
}
Basically what I want is to make it so that when the player is Aiming it changes the weapon position to ADS, but when the player is not aiming it changes it back. I tried to make it so that when aiming = Aim != null but it didnt work, I also tried other methods but I dont know what to do since I tried looking for a solution but didnt find any
And here's the code for invoking the method in the Player Script
if (Input.GetKey(aimKey))
{
aimInput?.Invoke();
}
The aimInput is a public static Action
I figured it out I just need to create another method called sideAimInput in the player, and it makes it set to aiming = false in the weaponInfo script
The script is attached to empty gameobject
At this line i'm using the mouse left button to fire a bullet one time.
If i'm using a break point it will shot one bullet once. but if i'm not using a break point it will shot two bullets in a row one after the other.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Cinemachine;
public class Shooting : MonoBehaviour
{
public CinemachineVirtualCamera cmf;
[Header("Main")]
public Rigidbody bulletPrefab;
public float launchForce = 700f;
public bool automaticFire = false;
public float bulletDestructionTime;
public bool go = false;
[Space(5)]
[Header("Slow Down")]
public float maxDrag;
public float bulletSpeed;
public bool bulletsSlowDown = false;
public bool overAllSlowdown = false;
[Range(0, 1f)]
public float slowdownAll = 1f;
public List<Transform> firePoints = new List<Transform>();
public Animator anim;
private void Start()
{
if (anim != null)
{
anim.SetBool("Shooting", true);
}
}
public void Update()
{
if (overAllSlowdown == true)
{
Time.timeScale = slowdownAll;
}
if (firePoints.Count > 0))
{
for (int i = 0; i < firePoints.Count; i++)
{
if (Input.GetMouseButton(0))
{
anim.SetTrigger("Shoot");
}
if (Input.GetMouseButton(1))
{
cmf.enabled = false;
}
if (go)
{
LaunchProjectile(firePoints[i]);
go = false;
}
}
}
}
private void LaunchProjectile(Transform firePoint)
{
Rigidbody projectileInstance = Instantiate(
bulletPrefab,
firePoint.position,
firePoint.rotation);
projectileInstance.transform.localScale = new Vector3(0.1f, 0.1f, 0.1f);
cmf.enabled = true;
cmf.Follow = projectileInstance.transform;
cmf.LookAt = projectileInstance.transform;
projectileInstance.AddForce(new Vector3(0, 0, 1) * launchForce);
if (bulletsSlowDown == true)
{
if (projectileInstance != null)
{
StartCoroutine(AddDrag(maxDrag, bulletSpeed, projectileInstance));
}
}
}
IEnumerator AddDrag(float maxDrag, float bulletSpeed, Rigidbody rb)
{
if (rb != null)
{
float current_drag = 0;
while (current_drag < maxDrag)
{
current_drag += Time.deltaTime * bulletSpeed;
rb.drag = current_drag;
yield return null;
}
rb.velocity = Vector3.zero;
rb.angularVelocity = Vector3.zero;
rb.drag = 0;
}
}
}
This script is attached to my player with animator and i'm using this method to reference event i added to animation in the animator controller. when the event happens the variable bool flag go is set to true.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ThrowObject : MonoBehaviour
{
public Shooting shooting;
public void ThrowEvent()
{
shooting.go = true;
}
}
This is a screenshot of the animator controller.
I added a new state name Throwing with two transitions from and to the Grounded state.
The Grounded state is playing idle animation.
In the transition from the Grounded to the Throwing i added a condition name Shoot type trigger.
In the transition from the Throwing state to the Grounded there is no any conditions.
Afaik animator triggers are stackable!
So since you call this in a for loop it might happen that it adds multiple triggers at once but each transition only consumes one at a time!
What I ended up using in combination with triggers in an animator is this script
public class AnimatorTriggerResetter : StateMachineBehaviour
{
override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
foreach(var p in animator.parameters)
{
if (p.type == AnimatorControllerParameterType.Trigger)
{
animator.ResetTrigger(p.name);
}
}
}
}
attach this to no specific state at all but directly to the Basic layer of your AnimatorController itself => It is called for each and every state that is entered => resets all existing triggers.
In your specific case though, why not rather pull the general calls out of the loop and rather make it
if (firePoints.Count > 0 && go)
{
if (Input.GetMouseButton(0))
{
anim.SetTrigger("Shoot");
}
if (Input.GetMouseButton(1))
{
cmf.enabled = false;
}
for (int i = 0; i < firePoints.Count; i++)
{
LaunchProjectile(firePoints[i]);
}
go = false;
}
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.
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.
Okay so before I begin, Yes I have looked online to find this answer. I've followed advice on multiple other questions, gone through the unity documentation, and done more than a few web searches and nothing I've found so far has solved the error. I'm sure someone will take one look at this and know immediately what's wrong, but as for me, I just can't find it.
Now that that's out of the way Here's the problem. I'm making a Breakout clone and I had everything done, everything working properly. I've got one static class that takes care of the scoring and score related variables, so that other scripts can access them easily. I wanted to practice some basic saving and loading with PlayerPrefs, so I added something for highscores. It's pretty much independent of the other classes, but once I finished that, I started getting a Null Reference Exception in a script that has been done for hours, and was working fine.
I appreciate any help you might have, and any tips for preventing this kind of error the next time around. Sorry It's such a long question.
Here's the full error:
NullReferenceException: Object reference not set to an instance of an object
MenuManager.ActivateLose () (at Assets/Scripts/MenuScripts/MenuManager.cs:31)
Scoring.CheckGameOver () (at Assets/Scripts/Scoring.cs:64)
Scoring.LifeLost () (at Assets/Scripts/Scoring.cs:51)
DeadZone.OnTriggerEnter2D (UnityEngine.Collider2D other) (at Assets/Scripts/DeadZone.cs:22)
And here are the three scripts listed in said error:
using UnityEngine;
using System.Collections;
public class DeadZone : MonoBehaviour
{
public GameObject ballPrefab;
public Transform paddleObj;
GameObject ball;
void Update ()
{
ball = GameObject.FindGameObjectWithTag("Ball");
}
void OnTriggerEnter2D(Collider2D other)
{
//if the object that entered the trigger is the ball
if(other.tag == "Ball")
{
Scoring.LifeLost();
//destroy it, and instantiate a new one above where the paddle currently is
Destroy(ball);
paddleObj.transform.position = new Vector2(0, -2.5f);
(Instantiate(ballPrefab, new Vector2(paddleObj.transform.position.x, paddleObj.transform.position.y + 0.3f), Quaternion.identity) as GameObject).transform.parent = paddleObj;
}
}
}
using UnityEngine;
using System.Collections;
public static class Scoring
{
public static GameObject scoreValue;
public static TextMesh scoreText;
public static int score;
static int multiplier = 0;
static int consecutiveBreaks = 0;
static int lives = 3;
static int totalBricks;
static int remainingBricks;
public static GameObject menuManagerObj;
public static MenuManager menuManager = new MenuManager();
static void Awake()
{
scoreValue = GameObject.FindGameObjectWithTag("Scoring");
scoreText = scoreValue.GetComponent<TextMesh>();
menuManagerObj = GameObject.FindGameObjectWithTag("MenuManager");
//menuManager = menuManagerObj.GetComponent<MenuManager>();
}
public static void BrickDestroyed()
{
if(scoreValue == null && scoreText == null)
{
scoreValue = GameObject.FindGameObjectWithTag("Scoring");
scoreText = scoreValue.GetComponent<TextMesh>();
}
remainingBricks--;
consecutiveBreaks++;
multiplier = 1 + (consecutiveBreaks % 5);
score += 10 * multiplier;
CheckGameOver();
scoreText.text = score + "";
}
public static void LifeLost()
{
consecutiveBreaks = 0;
multiplier = 1;
score -= 100;
lives--;
LivesDisplay.SetLives(lives);
CheckGameOver();
scoreText.text = score + "";
}
public static void SetBrickCount(int brickCount)
{
totalBricks = brickCount;
remainingBricks = totalBricks;
}
public static void CheckGameOver()
{
//lose condition
if(lives < 0) menuManager.ActivateLose();
//win condition
if(remainingBricks == 0) menuManager.ActivateWin();
}
}
using UnityEngine;
using System.Collections;
public class MenuManager : MonoBehaviour
{
public GameObject winMenu;
public GameObject loseMenu;
public GameObject pauseMenu;
public HighScoreManager highScores;
bool isGamePaused = true;
void Awake()
{
winMenu = GameObject.FindGameObjectWithTag("Win");
loseMenu = GameObject.FindGameObjectWithTag("Lose");
pauseMenu = GameObject.FindGameObjectWithTag("Pause");
}
public void ActivateWin()
{
Time.timeScale = 0f;
winMenu.transform.position = new Vector3(0, 0, -1);
highScores.CompareToHighScores(Scoring.score);
}
public void ActivateLose()
{
Time.timeScale = 0f;
loseMenu.transform.position = new Vector3(0, 0, -1);
}
void ActivatePause()
{
if(isGamePaused)
{
Time.timeScale = 0f;
pauseMenu.transform.position = new Vector3(0, 0, -1);
}
else
{
Time.timeScale = 1f;
pauseMenu.transform.position = new Vector3(35, 0, -1);
}
isGamePaused = !isGamePaused;
}
void Update()
{
if(Input.GetKeyDown(KeyCode.Escape))
{
ActivatePause();
}
}
}
The problem is that Scoring is not a MonoBehaviour so the Awake method never gets called. You can try to initialize the fields in a static constructor
static Scoring()
{
scoreValue = GameObject.FindGameObjectWithTag("Scoring");
scoreText = scoreValue.GetComponent<TextMesh>();
menuManagerObj = GameObject.FindGameObjectWithTag("MenuManager");
//menuManager = menuManagerObj.GetComponent<MenuManager>();
}
or make the Awake method public and call it from another MonoBehaviour.