Unity Dynamic UI number selection with plus and minus buttons - unity3d

while programming a game in Unity, I had troubles with incrementing this new feature: a dynamic UI number selection with plus and minus buttons.
What I want precisely:
1. three buttons, one blank in the middle displays the number, two with plus and minus signs which, when clicked, increment or decrement number by 1. WORKS OK!
image of what I did
2. (this is where it gets tricky) When user presses for more than, say, .2s, then it increments the value of the central button pretty fast as long as the user is still pressing on the button. Would avoid player from pressing hundred times the button because it increments only by 1.
3. Eventually add an acceleration phase of the increase (at the start increases by 3/s for example and at the max by 20/s or something like that)
Some help on this would be really great, thanks for those who will take time answering :)
edit: found somebody who asked same question on another post -->https://stackoverflow.com/questions/22816917/faster-increments-with-longer-uibutton-hold-duration?rq=1 but I don't understand a single inch of code...(doesn't look like c#) ;( help!

Are you using the button's OnClick() Event? It only calls the function once even if the user is holding down the key.
If you are not sure how to configure it. Here is a tutorial, you can use.
https://vionixstudio.com/2022/05/21/making-a-simple-button-in-unity/
Also the middle button can be a text field.

edit: found the solution myself. The principal issue I encountered when trying to make the button was a way to know if the button was being pressed (a bool variable). I already knew when it was clicked and when it was released with the OnPointerUp and OnPointerDown methods in the event trigger component. I then found the Input.GetButton() funct, which returns true if the button passed in parameter (here the mouse's left click) is pressed.
Here's the code (I didn't make an acceleration phase 'cause I was bored but it can be done pretty easily once you've got the Input.GetButton statement):
public class PlusAndMinus : MonoBehaviour
{
[SerializeField] private GameManager gameManagerScript;
[SerializeField] private int amount;
private bool isPressedForLong=false;
[SerializeField] private float IncreasePerSecWhenPressingLonger;
public void PointerDown()
{
if (!gameManagerScript.isGameActive)
{
StartCoroutine(PointerDownCoroutine());
}
}
private IEnumerator PointerDownCoroutine()
{
yield return new WaitForSeconds(.1f);#.2f might work better
if (Input.GetMouseButton(0))
{
isPressedForLong = true;
}
}
public void PointerUp()
{
if (!gameManagerScript.isGameActive)
{
isPressedForLong = false;
gameManagerScript.UpdateNumberOfBalls("Expected", amount);
}
}
private void Update()
{
if (isPressedForLong)
{
gameManagerScript.UpdateNumberOfBalls("Expected", amount * IncreasePerSecWhenPressingLonger * Time.deltaTime);
}
}
}
The PointerDown event in the event trigger component calls the PointerDown function in the script (same for PointerUp).
The value of "amount" var is set to 1 for the plus button and -1 for the minus button.
The two if statements checking if game is not active are for my game's use but aren't necessary.
Finally, the gameManagerScript.UpdateNumberOfBalls("expected",amount); statements call a function that updates the text in the middle by the amount specified. Here's my code ("expected" argument is for my game's use):
#inside the function
else if (startOrEndOrExpected == "Expected")
{
if (!((numberOfBallsExpected +amount) < 0) & !((numberOfBallsExpected +amount) > numberOfBallsThisLevel))
{
if (Math.Round(numberOfBallsExpected + amount) != Math.Round(numberOfBallsExpected))
{
AudioManager.Play("PlusAndMinus");
}
numberOfBallsExpected += amount;
numberOfBallsExpectedText.text = Math.Round(numberOfBallsExpected).ToString();
}
}
0 and numberOfBallsThisLevel are the boundaries of the number displayed.
Second if statement avoids the button click sound to be played every frame when the user presses for long on the button (only plays when number increments or decrements by 1 or more).
Hope this helps!

Related

Enabling button after 100 clicks

so I wanted to create a secret or something in my game. Basically what i wanted to ask is how do you make a button work after clicking it a 100 times. After clicking a 100 times it should give me 100 money. Im new to unity so it would be nice if you could explain it more precise. I tried this code:
using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
public class SecretButton : MonoBehaviour
{
public int clickcounter = 0;
public TMP_Text scoreText;
int moneyAmount;
void Start()
{
}
void Update()
{
if (clickcounter == 100)
{
moneyAmount += 100;
PlayerPrefs.SetInt("MoneyAmount", moneyAmount);
moneyAmount = PlayerPrefs.GetInt("MoneyAmount");
scoreText.text = moneyAmount.ToString();
}
}
public void increaseClicks()
{
clickcounter++;
}
}
but it started giving me 100 money all the time, but i wanted it to give me only once 100 money.
Easiest way I can think of is creating a variable _counter assigned to the button and increase it by one for each button press.
inside the button press function, include an if statement that checks whether the _counter>100 and in that case it starts the followup actions, otherwise it skips the action.
The answer is quite high level, but if you want more details, you have to share more details about what you are trying to accomplish
update:
I can see in your edit the code now, your additional problem with giving 100 money multiple times is because you don't make this function execute only once.
You can solve this by adding another variable _secret_available and initiate it as True, when the function is activated, set the variable to False.
You also need to edit the if clause to be like this:
if(counter ==100 && _secret_available)
Also my suggestion would be to replace counter==100 with counter >=100 so that you don't have possible bugs in case the counter jumps from 99 to 101 without passing by 100. Might not be a problem in the moment but I believe it is more robust

After pause menu score doesn't work anymore

does anyone know how to fix this? In my unity game when I go to pause menu and then continue playing, my scoreboard stops updating. I have two scoreboards, one in game and one in pause menu. The one in pause menu works well and updates but the one in game freezes after once visited in pause menu.
Here is my pausecodes and codes to add money (score):
public void PauseGame()
{
Time.timeScale = 0;
}
public void UnPauseGame()
{
Time.timeScale = 1;
}
}
if (collision.gameObject.tag == "Respawn") // When player lifts fish up
{
Destroy(this.gameObject);
// TODO: Player gets money (points) when this happens
textManager.instance.AddMoney();
Debug.Log("Add money");
}
public class textManager : MonoBehaviour
{
public static textManager instance;
public Text moneyText;
public int money;
private void Awake()
{
instance = this;
}
// Start is called before the first frame update
void Start()
{
Data data = SaveSystem.LoadData();
money = data.balance;
moneyText.text = "Balance: " + money.ToString() + " $";
}
public void AddMoney()
{
money = money + 10;
moneyText.text = "Balance: " + money.ToString() + " $";
SaveSystem.SaveData(this);
}
public int findMoney()
{
return money;
}
}
Please ask more info if needed.
I have tried to delete the one scoreboard in pause menu and after that the in-game pause menu started working right, but I would like to have still that other scoreboard too.
if (collision.gameObject.tag == "Respawn") // When player lifts fish up
{
// TODO: Player gets money (points) when this happens
textManager.instance.AddMoney();
Debug.Log("Add money");
Destroy(this.gameObject);
}
As I said this is the fix for what you posted as for the pause unpause problem you have to post the actual code where you do the pause unpause behavior so can people help you out my friend. As for what you posted what you've been doing is destroying the script just before excuting the call to textManager.instance.AddMoney(); and this will never run in the order you set in your code.
The scope of a static field is global, this means there can only be one.
Your textManager (side note: a classes first letter has to be upper case) is certainly attached to multiple GameObjects, once to the object displaying the score in the scene UI and once to a UI element in the pause menu.
As soon as you pause your game the first time, the TextManager attached to the object in the pause menu will run Awake() and the instance (please capitalise your properties as well) will be overriden and the reference to the ingame TextManager gets discarded.
The sloppy fix is re-initializing the ingame TextManager when you unpause the game, assigning it to be the Instance again. I'd not recommend doing that though.
The better solution is to implement an event on the player that gets triggered when the score changes and making the player instance a Singleton object since there will be only one player in all circumstances.
The UI elements displaying the score can then subscribe to this event in OnEnable() and unsubscribe in OnDisable() (do not forget to unsubscribe from events).
Addendum: You should not destroy your object before all code has been executed. Your code will still work because of how things are managed on the C++ layer of Unity, but it is definitely bad practice.

How to click on instantiated objects and get points, then link points to score text?

What I want:
I want the player to be able to click on instantiated objects and get points, then have those points show in the score-keeping text.
What I’ve done:
I’m currently using the following “FindGameObjectsWithTag” code to retrieve the buttons that are components of the instantiated prefab objects:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
public class CPointScore : MonoBehaviour
{
public TextMeshProUGUI CPointsText;
private float ScoreNum;
private GameObject[] CButtonGmeObjsHolder;
private void CTagFinder()
{
CButtonGmeObjsHolder = GameObject.FindGameObjectsWithTag("Ctag");
foreach (GameObject CButtonGmeObj in CButtonGmeObjsHolder)
{
Debug.Log("GmeObj Found");
Button CButton = CButtonGmeObj.GetComponent<Button>();
CButton.onClick.AddListener(AddScore);
}
}
public void AddScore()
{
ScoreNum += 1;
Debug.Log("Point Added # " + ScoreNum);
}
void Start()
{
InvokeRepeating("CTagFinder", 1f, 15.1f);
}
void Update()
{
CPointsText.text = ScoreNum.ToString();
}
}
Because FindGameObjectsWithTag only calls once I have the InvokeRepeating code in start. I have game objects spawning throughout the duration of the game so it needs to be constantly checking for tags.
Issue:
So the code finds the tags, the buttons are able to be clicked, and the score-keeping text updates which is great. The problem is that if I click one tagged button it will register a point for itself and every tagged button currently in the scene that spawned after it. For example, lets say I have 4 spawned objects currently on scene, when the first object spawned is clicked it will add 4 points instead of 1. If the second object spawned is clicked it will add 3 points instead of 1. I would like to have only the tagged button that is clicked register a point.
Question:
What can I change in my code so that only the tagged button that is clicked registers a point?
Thank you
I think there are two things here:
You repeatedly add the listener so you will end up with multiple callbacks when the button is finally clicked.
The repeated FindGameObjectsWithTag is also quite inefficient
Your main issue is the repeated calling.
For each repeated call of CTagFinder you go through all existing buttons and do
CButton.onClick.AddListener(AddScore);
so these existing buttons end up with multiple listeners attached!
You either want to make sure it is only called once per button, e.g. keeping track of those you already did this for:
private readonly HashSet<Button> alreadyRegisteredButtons = new HashSet<Button>();
and then
if(!alreadyRegisteredbuttons.Contains(CButton))
{
CButton.onClick.AddListener(AddScore);
alreadyRegisteredButtons.Add(CButton);
}
or alternatively make sure you remove the callback before you add it like
CButton.onClick.RemoveListener(AddScore);
CButton.onClick.AddListener(AddScore);
In general I would not use FindGameObjectWithTag an poll objects repeatedly. Rather make your code event driven. This would already avoid the issue at all since there would be no repeated attaching of the listener anyway.
I would simply have a dedicated component YourComponent attached to the same GameObject as the buttons and have a global
public static event Action<YourComponent> OnCTSButtonSpawned;
and in this dedicated component do
private void Start()
{
OnCTSButtonSpawned?.Invoke(this);
}
and in your CPointScore listen to this event like
private void Awake()
{
YourComponent.OnCTSButtonSpawned += AttachListener;
}
private void AttachListener(YourComponent component)
{
if(compoenent.TryGetComponent<Button>(out var button))
{
button.onClick.AddListener(AddScore);
}
}
private void AddScore()
{
ScoreNum++;
CPointsText.text = ScoreNum.ToString();
}

In Unity, how to detect if window is being resized and if window has stopped resizing

I wanted my UI to not resize when user is still resizing the game (holding click in the window border) and only when the user has released the mouse the resize event will trigger.
I have tried to achieve it on Unity but so far I only able to detect windows size change, which my script checked every 0.5 second and if detected change it will resize the UI. But of course resizing everything caused a heavy lag, so resizing every 0.5 second is not a good option but resizing every 1 second is not a good idea either because 1 second is considered too long.
The question might be too broad but I have specified the problem as small as possible, how do I detect if user is still resizing the window? And how do I detect if user has stopped resizing the window (stop holding click at window border)?
You can't tell when someone stops dragging a window, unless you want to code a low level solution for ever desktop environment and every operating system.
Here's what worked for me with any MonoBehavior class, using the OnRectTransformDimensionsChange event:
public class StackOverflow : MonoBehaviour
{
private const float TimeBetweenScreenChangeCalculations = 0.5f;
private float _lastScreenChangeCalculationTime = 0;
private void Awake()
{
_lastScreenChangeCalculationTime = Time.time;
}
private void OnRectTransformDimensionsChange()
{
if (Time.time - _lastScreenChangeCalculationTime < TimeBetweenScreenChangeCalculations)
return;
_lastScreenChangeCalculationTime = Time.time;
Debug.Log($"Window dimensions changed to {Screen.width}x{Screen.height}");
}
}
I have some good news - sort of.
When the user resizes the window on Mac or PC,
Unity will AUTOMATICALLY re-layout everything.
BUT in fact ONLY when the user is "finished" resizing the Mac/PC window.
I believe that is what the OP is asking for - so the good news, what the OP is asking for is quite automatic.
However. A huge problem in Unity is Unity does not smoothly resize elements as the user is dragging the mouse to expand the Mac/PC window.
I have never found a solution to that problem. (A poor solution often mentioned is to check the size of the window every frame; that seems to be about the only approach.)
Again interestingly, what the OP mentions
" ..and if window has stopped resizing .."
is automatically done in Unity; in fact do nothing to achieve that.
I needed something like this for re generating a line chart, but as it has too many elements, it would be heavy to do it on every update, so I came up with this, which for me worked well:
public class ToRunOnResize : MonoBehaviour
{
private float screenWidth;
private bool screenStartedResizing = false;
private int updateCounter = 0;
private int numberOfUpdatesToRunXFunction = 15; // The number of frames you want your function to run after, (usually 60 per second, so 15 would be .25 seconds)
void Start()
{
screenWidth = Screen.width; // Identifies the screen width
}
private void Update()
{
if (Screen.width != screenWidth) // This will be run and repeated while you resize your screen
{
updateCounter = 0; // This will set 0 on every update, so the counter is reset until you release the resizing.
screenStartedResizing = true; // This lets the application know when to start counting the # of updates after you stopped resizing.
screenWidth = Screen.width;
}
if (screenStartedResizing)
{
updateCounter += 1; // This will count the updates until it gets to the numberOfUpdatesToRunXFunction
}
if (updateCounter == numberOfUpdatesToRunXFunction && screenStartedResizing)
{ // Finally we make the counter stop and run the code, in my case I use it for re-rendering a line chart.
screenStartedResizing = false;
// my re-rendering code...
// my re-rendering code...
}
}
}

Create a Megaman like character selection screen with Unity2D

I want to create a character selection screen like the old Megaman games beside that i want to create the menus for the game my question is how do i navigate through game objects using the keyboard or the Dpad and selecting them?
I dont want to use UI elements but box colliders and such can anyone please provide an example how to achieve this?
I followed other Unity tutorials however it uses UI buttons.
Thanks for the help.
1.Name all your NonUIButtons in ascending order [eg: Button1,Button2,etc.,]
2.Enable the first button on Awake()
void Awake(){
currentValue=0;
GameObject goToEnable = GameObject.Find("NonUIButton1");
goToEnable.SetActive(true);
}
3.Get the list of allButtons
public List<GameObject> items = new List<GameObject>();
void Start(){
items.AddRange(GameObject.FindGameObjectWithTag("NonUIButtons");}
4.When ever key is pressed calcualte the movement using simple additions and subtraction, disable all Buttons and Enable the only one you want
void Update(){
if (Input.GetKeyDown("Up"))
{
currentValue -= 3; // Assuming each row has 3 buttons
foreach(GameObject obj in items){
obj.SetActive(false);
}
}// DO similar steps for each type of movement also make sure to add edge cases
}
Enabling the Button you want
GameObject goToEnable = GameObject.Find("NonUIButton1");
goToEnable.SetActive(true);