I am trying to create a simple dialogue system for my game in Unity. I've set up a special trigger for a Dialogue to start and the code is passing right variable but somehow it gets stuck at clearing the queue and throws a NullReferenceException.
I've seen through debugger that all variables and triggers work perfectly fine till cs:27 inside DialogueManager.cs. I've also checked the inspector to make sure everything is correctly assigned.
Dialogue class:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[System.Serializable]
public class Dialogue
{
public string name;
[TextArea(3,10)]
public string[] sentences;
}
DialogueTrigger class:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class NPC_DialogTrigger : MonoBehaviour
{
// Player
public Transform player;
// GameMaager to close
public GameObject Gameplay;
// Camera and Canvas to turn on
public GameObject DialogueManager;
// NPC Details
public GameObject InteractionNPCNameTextField;
public Transform interactionTransform;
public float radius = 3f;
private bool isBusy = false;
// DialogueStart
public GameObject DialogueStart;
void Start()
{
InteractionNPCNameTextField.gameObject.SetActive(false);
}
void Update()
{
float distance = Vector3.Distance(player.position, interactionTransform.position);
if (distance <= radius)
{
if (isBusy == false)
{
InteractionNPCNameTextField.gameObject.SetActive(true);
if (Input.GetKeyDown(KeyCode.E))
{
Dialogue();
Debug.Log("Started Dialogue procedure.");
}
}
}
else if (distance > radius)
{
InteractionNPCNameTextField.gameObject.SetActive(false);
}
}
public void Dialogue()
{
Gameplay.SetActive(false);
DialogueManager.SetActive(true);
DialogueStart.GetComponent<DialogueStart>().TriggerDialogue();
Debug.Log("Triggered Dialogue.");
}
void OnDrawGizmosSelected()
{
if (interactionTransform == null)
{
interactionTransform = transform;
}
Gizmos.color = Color.yellow;
Gizmos.DrawWireSphere(interactionTransform.position, radius);
}
}
DialogueStart class:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class DialogueStart : MonoBehaviour
{
public Dialogue dialogue;
public void TriggerDialogue()
{
FindObjectOfType<DialogueManager>().StartDialogue(dialogue);
Debug.Log("Dialogue sent to dialogue manager.");
}
}
DialogueManager class:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
public class DialogueManager : MonoBehaviour
{
public Text nameText;
public Text DialogueText;
private Queue<string> sentences;
public GameObject DialogueManagerUI;
void Start()
{
if (sentences == null)
{
sentences = new Queue<string>();
}
}
public void StartDialogue (Dialogue dialogue)
{
Debug.Log("Received dialogues: " + dialogue);
nameText.text = dialogue.name;
Debug.Log("Start Dialogue: " + sentences.Count);
sentences.Clear();
Debug.Log("Sentences Cleared: " + sentences);
foreach (string sentence in dialogue.sentences)
{
sentences.Enqueue(sentence);
}
DisplayNextSentence();
}
public void DisplayNextSentence()
{
if(sentences.Count == 0)
{
EndDialogue();
return;
}
string sentence = sentences.Dequeue();
DialogueText.text = sentence;
}
void EndDialogue()
{
Debug.Log("End of conversation.");
}
private void Update()
{
if (DialogueManagerUI.activeInHierarchy)
{
Cursor.lockState = CursorLockMode.None;
Cursor.visible = true;
}
else if (!DialogueManagerUI.activeInHierarchy)
{
Cursor.lockState = CursorLockMode.Locked;
Cursor.visible = false;
}
}
}
Visual Studio doesn't give me any errors.
Unity error code -> Screenshot
Debugger right before issue -> Screenshot2
Seems to me like the Queue is never assigned to sentences and it remains empty.
If it is so - why?
It sounds like DialogueManager.StartDialogue probably via DialogueStart.TriggerDialogue is called from somewhere in either Awake or at least before your Start was executed.
Especially
DialogueManager.SetActive(true);
lets assume the DialogueManager object is inactive at first anyway. So maybe your stuff is called before it is set to active.
This might also be due to the Start where you set
InteractionNPCNameTextField.gameObject.SetActive(false);
so any component on this GameObject might not have its Start method getting called. Maybe you referenced the wrong GameObject here?
Debugging would help to figure out in which order your methods get called.
In general my thumb rule for initializing is
Do everything that depends only on yourself in Awake
Do everything that depends on other components being setup already in Start
This way you can (almost) always be sure that stuff is already initialized when you need it.
However, these are only guesses but
The simple solution here:
You could already solve this by simply initializing the sentences right away using
private readonly Queue<string> sentences = new Queue<string>();
now it is definitely initialized even before Start or Awake get called!
Related
I have been trying to make an OnTriggerEnter Score system and it has not been updating and is showing no errors so am I doing something wrong that I dont know about here is my Code:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TMPro;
public class ScoreManger : MonoBehaviour
{
public TextMeshProUGUI MyscoreText;
private int ScoreNum;
// Start is called before the first frame update
void Start()
{
ScoreNum = 0;
MyscoreText.text = ScoreNum.ToString();
}
void update() {
MyscoreText.text = ScoreNum.ToString();
}
public void OnTriggerEnte2D(Collider2D col){
if(col.tag == "Score"){
ScoreNum += 1;
Debug.Log("It Worked");
MyscoreText.text = ScoreNum.ToString();
Destory(col.gameObject);
}
}
}
You spelled OnTriggerEnter2D wrong.
Or you haven't marked at least one of the colliders you are interacting with as a trigger.
Or you haven't attached the script to a gameobject.
I'm trying to set up a dash ability that updates the player velocity, waits a set amount of time (dashTime) and then sets the canDash to false for another set amount of time (dashCooldown).
However the waitForSeconds don't seem to be working as the canDash bool only remains false for a split second before switching back to true.
PlayerAbilities.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerAbilities : MonoBehaviour
{
// Scripts & Data
[SerializeField] PlayerController s_PC;
internal PlayerData pd = new PlayerData();
private void Awake() {
pd.canDash = true;
}
// Dash
internal IEnumerator Dash()
{
pd.CanDash = false;
Debug.Log("dash, dash baby");
// Perform dash and update canDash
s_PC.rb2d.velocity = s_PC.s_PlayerMovement.moveInput * pd.dashSpeed;
yield return new WaitForSeconds(pd.dashTime); // Wait set time for dash to happen
s_PC.UpdateState(PlayerController.State.Normal); // Change state back to normal
// Cooldown dash
yield return new WaitForSeconds(pd.dashCooldown); // Cooldown dash
pd.CanDash = true;
}
}
PlayerController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerController : MonoBehaviour
{
// Scripts & Data
[SerializeField] internal PlayerAbilities s_PlayerAbilities;
[SerializeField] internal PlayerInput s_PlayerInput;
internal PlayerData pd = new PlayerData();
// Update is called once per frame
private void FixedUpdate()
{
// Check if user has control of player
if (pd.canControlPlayer)
{
// Update player state
switch (playerState)
{
case State.Normal:
s_PlayerMovement.Move();
break;
case State.Dash:
StartCoroutine(s_PlayerAbilities.Dash());
break;
case State.DodgeRoll:
StartCoroutine(s_PlayerAbilities.DodgeRoll());
break;
}
}
}
}
PlayerData.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
[Serializable]
public class PlayerData
{
// Dash
internal float dashSpeed = 50f;
internal float dashTime = 0.2f;
internal float dashCooldown = 5f;
internal bool canDash = true;
internal bool CanDash {
get { return canDash; }
set { canDash = value; }
}
}
PlayerInput.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerInput : MonoBehaviour
{
// Scripts & Data
[SerializeField] PlayerController s_PC;
internal PlayerData pd = new PlayerData();
// Input Actions
internal PlayerInputActions playerInputActions;
private void Awake() {
// Define new player input
playerInputActions = new PlayerInputActions();
}
private void Update()
{
// Check if player state is normal
if (s_PC.playerState == PlayerController.State.Normal)
{
// Check for button presses
if (playerInputActions.Movement.DodgeRoll.triggered)
s_PC.UpdateState(PlayerController.State.DodgeRoll);
}
}
}
The code is so unnecessarily complicated, you're just making one of a zillion simple mistakes somewhere
Follow normal debugging.
To begin with change this
yield return new WaitForSeconds(pd.dashCooldown);
to this
Debug.Log("test with number");
yield return new WaitForSeconds(20f);
Debug.Log("tested with number");
and go from there.
BTW there's something completely wrong at FixedUpdate(). For some reason you're call Dash (and others) every frame. That seems totally wrong. You'd call that sort of thing once on key down.
ALSO:
You've probably suffered this Gotchya:
https://stackoverflow.com/a/35166004/294884
due to all the bizarre variables.
I've been stuck at this for a while. What I want is for my outline object to be instantiated at the location of my bronze Base game object and for it to destroy when the bronze base is no longer the closest to the player.
I'm willing to completely restart my bronze script if it means I can make this easier.
Thanks in advance!
Find Closest Bronze Script
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using UnityEngine;
public class FindBronze : MonoBehaviour
{
void Update()
{
FindClosestBronze();
}
void FindClosestBronze()
{
float distanceToClosestBronze = Mathf.Infinity;
Bronze closestBronze = null;
Bronze[] allBronze = GameObject.FindObjectsOfType<Bronze>();
foreach (Bronze currentBronze in allBronze)
{
float distanceToBronze = (currentBronze.transform.position - this.transform.position).sqrMagnitude;
if (distanceToBronze < distanceToClosestBronze)
{
distanceToClosestBronze = distanceToBronze;
closestBronze = currentBronze;
}
if (distanceToBronze > distanceToClosestBronze)
{
closestBronze.GetComponent<Bronze>().notSelected();
}
closestBronze.GetComponent<Bronze>().Selected();
}
}
}
Bronze (includes outline) script
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Bronze : MonoBehaviour
{
public bool isSelected = false;
public Animator anim;
[SerializeField]
public GameObject selectedBox;
public GameObject bronzeBase;
private GameObject clone;
// Update is called once per frame
void Awake()
{
clone = (GameObject)Instantiate(selectedBox, bronzeBase.transform);
}
public void Selected()
{
if (!isSelected)
{
clone = (GameObject)Instantiate(selectedBox, bronzeBase.transform);
isSelected = true;
}
else
{
Destroy(clone);
isSelected = false;
}
}
public void notSelected()
{
Destroy(selectedBox);
}
}
In the Bronze in notSelected you are destroying the prefab selectBox!
You probably rather wanted to destroy the clone instance.
Anyway I would suggest a few things that I would do different
Instead of Instantiate and Destroy all the time rather only use SetActive
Instead of using FindObjectOfType in Update store them in a HashSet event driven: Each Bronze instance registers and unregisters itself
Depends on personal taste but I would use Linq to find the closest instance
This could look somewhat like
public class Bronze : MonoBehaviour
{
// Every instance of this component registers and unregisters itself here
public static readonly HashSet<Bronze> Instances = new HashSet<Bronze>();
[Header("References")]
public Animator anim;
[SerializeField] private GameObject selectedBox;
[SerializeField] private GameObject bronzeBase;
[Header("Debugging")]
[SerializeField] bool _isSelected = false;
private GameObject clone;
// Have a property for the selection
public bool IsSelected
{
// when something reads this property return _isSelected
get => _isSelected;
// This is executed everytime someone changes the value of IsSelected
set
{
if(_isSelected == value) return;
_isSelected = value;
clone.SetActive(_isSelected);
}
}
// Update is called once per frame
void Awake()
{
// Instantiate already returns the type of the given prefab
clone = Instantiate(selectedBox, bronzeBase.transform);
// Register yourself to the alive instances
Instances.Add(this);
}
private void OnDestroy ()
{
// Remove yourself from the Instances
Instances.Remove(this);
}
}
And then use it
using System.Linq;
public class FindBronze : MonoBehaviour
{
private Bronze currentSelected;
private void Update()
{
UpdateClosestBronze();
}
void UpdateClosestBronze()
{
if(Bronze.Instances.Count ==0) return;
// This takes the instances
// orders them by distance ascending using the sqrMagnitude
// of the vector between the Instance and you
// sqrMagnitude is more efficient than Vector3.Distance when you only need to compare instead of the actual distance
// then finally it takes the first item
var newClosest = Bronze.Instances.OrderBy(b => (b.transform.position - transform.position).sqrMagnitude).First();
// skip if the result is the same as last time
if(newClosest == currentSelected) return;
// otherwise first deselect the current selection if there is one
if(currentSelected)
{
currentSelected.IsSelected = false;
}
// Then set the new selection
currentSelected = newSelected;
currentSelected.IsSelected = true;
}
}
I want to have the player start from a start position at first. Then start from a checkpoint after they've completed level one. I've tried different iterations of this script, it is supposed to use a bool check if the player has passed through a trigger, and if they have their position will be equal to the checkpoint at Start. It has to remember the bool when the game is turned off and on again.
Right now the player always starts from the checkpoint.
I tried making my own boolean with playprefs but ended up using BoolPrefs, which still didn't work.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Player : MonoBehaviour
{
public Rigidbody player;
public Transform startPoint;
public Transform checkPoint;
private void Start()
{
if (PlayerPrefsX.GetBool("level01Complete", false))
{
player.transform.position = startPoint.position;
}
if (PlayerPrefsX.GetBool("level01Complete", true))
{
player.transform.position = checkPoint.position;
}
}
private void OnTriggerEnter(Collider other)
{
if (other.gameObject.tag == "HubTrigger")
{
PlayerPrefsX.SetBool("level01Complete", true);
}
else
{
PlayerPrefsX.SetBool("level01Complete", false);
}
}
}
Here is the BoolPrefs script I'm using currently.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerPrefsX
{
public static void SetBool(string name, bool booleanValue)
{
PlayerPrefs.SetInt(name, booleanValue ? 1 : 0);
}
public static bool GetBool(string name)
{
return PlayerPrefs.GetInt(name) == 1 ? true : false;
}
public static bool GetBool(string name, bool defaultValue)
{
if (PlayerPrefs.HasKey(name))
{
return GetBool(name);
}
return defaultValue;
}
}
I am new to using PlayerPrefs so there may just be something obvious that I'm overlooking.
HubTrigger is the trigger you pass through after completing level one. So it should be that the first time you play you start from one spot, then once you pass through hubtrigger, you've completed level one and from then on you start from the checkpoint when you load the game.
PlayerPrefsX looks fine. It just seems like you're using it incorrectly. You should check if (PlayerPrefsX.GetBool("level01Complete", false)), and if that's true, then set the checkpoint position, and set the start position otherwise:
private void Start()
{
if (PlayerPrefsX.GetBool("level01Complete", false))
{
player.transform.position = checkPoint.position;
}
else
{
player.transform.position = startPoint.position;
}
}
For OnTriggerEnter, you really only should set level01Complete there in the event you beat level 1:
private void OnTriggerEnter(Collider other)
{
if (other.gameObject.tag == "HubTrigger")
{
PlayerPrefsX.SetBool("level01Complete", true);
}
}
I'm trying to be able to respawn as well as reset the score counting as soon as i walk into my "Gold" Object.
As for now i'm not even able to respawn which was possible earlier before trying to implement the "Score-Stuff" (at first the "FoundGold" Script was only used to be able to respawn). Also i'm trying to make the lowest Score the High-Score.
Note that im new to C# and i kinda put everything together from the tutorials i needed so a answer with some actual code/stating where something went wrong would be much appreciated.
//GoldFound Code
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GoldFound : MonoBehaviour
{
private ScoreManager theScoreManager;
public Transform target;
[SerializeField] private Transform player;
[SerializeField] private Transform respawnpoint;
private void Start()
{
theScoreManager = FindObjectOfType<ScoreManager>();
}
private void OnTriggerEnter(Collider other)
{
theScoreManager.scoreIncreasing = false;
player.transform.position = respawnpoint.transform.position;
theScoreManager.scoreCount = 0;
theScoreManager.scoreIncreasing = true;
}
}
other code
//ScoreManager
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class ScoreManager : MonoBehaviour
{
public Text scoreText;
public Text hiScoreText;
public float scoreCount;
public float hiScoreCount;
public float pointPerSecond;
public bool scoreIncreasing;
// Use this for initialization
void Start()
{
}
// Update is called once per frame
void Update()
{
if (scoreIncreasing)
{
scoreCount += pointPerSecond * Time.deltaTime;
}
if(scoreCount > hiScoreCount)
{
hiScoreCount = scoreCount;
}
scoreText.text = "Score: " + Mathf.Round (scoreCount);
hiScoreText.text = "High Score: " + Mathf.Round (hiScoreCount);
}
}
If you want to save your highscore in-between play sessions, then the easiest way to do so is to save the value to PlayerPrefs. If you want to start saving more / more complex stuff you really should save it in a file you generate yourself. But in your case, PlayerPrefs is fine.
Here's a Unity tutorial on the subject:
https://unity3d.com/learn/tutorials/topics/scripting/high-score-playerprefs
Otherwise, you can just do it like this:
public void SetHighscore (float currentScore)
{
if (PlayerPrefs.HasKey("highscore"))
{
float highscore = PlayerPrefs.GetFloat("highscore");
if (highscore > currentScore)
{
PlayerPrefs.SetFloat("highscore", currentScore);
PlayerPrefs.Save();
}
}
else
{
PlayerPrefs.SetFloat("highscore", currentScore);
PlayerPrefs.Save();
}
}
Then just write PlayerPrefs.GetKey("highscore") whenever you need it.
(Though I'd also recommend you check if it exists by using the PlayerPrefs.HasKey("highscore"))
https://docs.unity3d.com/ScriptReference/PlayerPrefs.html