day/night cycle and also track days? - unity3d

I'm trying to make a day and night cycle that also keeps track of how many days passed. I watched a tutorial to start and for this post, I removed all the unnecessary stuff like rotating the sun and changing colors.
public class TimeController : MonoBehaviour
{
[SerializeField]
private float timeMultiplier;
[SerializeField]
private float startHour;
private DateTime currentTime;
void Start()
{
currentTime = DateTime.Now.Date + TimeSpan.FromHours(startHour);
}
void Update()
{
UpdateTimeOfDay();
}
private void UpdateTimeOfDay()
{
currentTime = currentTime.AddSeconds(Time.deltaTime * timeMultiplier);
}
}
The biggest issue I have with this is instead of timeMultiplier I'd rather have like a dayLength variable which represents how long a day should last in minutes so one day is 10minutes int dayLength = 600; or something like that, but I'm not sure how I'd implement that.
Also, I tried adding a method for checking if a day has passed, but the code ended up running like 20 times.
public int currentDay = 1;
private void HasDayPassed()
{
if (currentTime.Hour == 0)
{
Debug.Log("New day");
currentDay++;
}
}

Try this code
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class TimeController : MonoBehaviour
{
public const float SecondsInDay = 86400f;
[SerializeField] [Range(0, SecondsInDay)] private float _realtimeDayLength = 60;
[SerializeField] private float _startHour;
private DateTime _currentTime;
private DateTime _lastDayTime;
private int _currentDay = 1;
private void Start()
{
_currentTime = DateTime.Now + TimeSpan.FromHours(_startHour);
_lastDayTime = _currentTime;
}
private void Update()
{
// Calculate a seconds step that we need to add to the current time
float secondsStep = SecondsInDay / _realtimeDayLength * Time.deltaTime;
_currentTime = _currentTime.AddSeconds(secondsStep);
// Check and increment the days passed
TimeSpan difference = _currentTime - _lastDayTime;
if(difference.TotalSeconds >= SecondsInDay)
{
_lastDayTime = _currentTime;
_currentDay++;
}
// Next do your animation stuff
AnimateSun();
}
private void AnimateSun()
{
// Animate sun however you want
}
}
Also you can remove the range attribute to specify more slower day than the realtime day.

Related

Changing a variable of function from another script

Hello everyone I have a question. I have 2 script files on Unity.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Rendering.Universal;
public class CycleFinal : MonoBehaviour
{
//Lights
public Light2D GlobLight;
public Light2D SptLight;
//Displays
public Text timeDisplay;
public Text moveDisplay;
public Text phaseDisplay;
//Others
[SerializeField]
public float cdTimer = 20f;
public float moveCount = 20f;
void Start()
{
timeDisplay.GetComponent<Text>();
moveDisplay.GetComponent<Text>();
phaseDisplay.GetComponent<Text>();
moveCount = 0f;
DayTime();
}
void Update()
{
cdTimer -= Time.deltaTime;
if(cdTimer <0){
NightTime();
}else if(moveCount <0){
DayTime();
}
timeDisplay.text = "Day Time left: " + Mathf.Round(cdTimer);
moveDisplay.text = "Movement left: " + Mathf.Round(moveCount);
}
public void DayTime(){
phaseDisplay.text = "Day";
SptLight.intensity = 0;
GlobLight.intensity = 1;
moveCount = 0;
}
public void NightTime(){
phaseDisplay.text = "Night";
SptLight.intensity = 1;
GlobLight.intensity = 0;
moveCount = 20;
cdTimer = 0;
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Tilemaps;
public class PlayerController : MonoBehaviour
{
public CycleFinal script;
private PlayerMovement controls;
[SerializeField]
private Tilemap groundTilemap;
[SerializeField]
private Tilemap collisionTilemap;
private void Awake() {
controls = new PlayerMovement();
}
private void OnEnable() {
controls.Enable();
}
private void OnDisable() {
controls.Disable();
}
void Start()
{
controls.Main.Movement.performed += ctx => Move(ctx.ReadValue<Vector2>());
}
private void Move(Vector2 direction){
if(CanMove(direction))
transform.position += (Vector3)direction;
}
private bool CanMove(Vector2 direction){
Vector3Int gridPosition = groundTilemap.WorldToCell(transform.position + (Vector3)direction);
if(!groundTilemap.HasTile(gridPosition) || collisionTilemap.HasTile(gridPosition) || script.moveCount <= 0)
return false;
else
return true;
}
}
I am trying to decrease moveCount variable on every move my character made. (I'm planing to do this thing on PlayerController script Move function.) I accomplished this while moveCount variable is a variable outside of the NightTime function but when I move that moveCount variable in to the NightTime function I couldn't change its value. How can I do that?
Also main thing I want to do with those 2 script is the player will have x seconds of day time and x moves on nighttime when x seconds ends the game turns into night time and when x moves ends the game turns into day time.

Unity timer count down not working when app killed android

I'm new to unity and c#, I have a code to increase energy after 10m, I have used PlayerPrefs to do so it is working in unity but not working on mobile please help.
I'm unable to understand what exactly is causing problem in phone. Also the timer pauses when the app is not killed but just minimized on mobile. I want the timer countdown to keep going game minimized and also if killed.
using System;
using UnityEngine;
using UnityEngine.UI;
public class EnergyAdder : MonoBehaviour
{
// Start is called before the first frame update
public float waitTime = 600;
Timer timer;
public GameRoot gameRoot;
public int count;
DateTime currentDate;
DateTime oldDate;
void Start()
{
//timer = new Timer(waitTime);
float remainingTime = PlayerPrefs.GetFloat("TimeOnExit");
timer = new Timer(remainingTime);
if (PlayerPrefs.HasKey("sysString"))
{
currentDate = DateTime.Now;
long temp = Convert.ToInt64(PlayerPrefs.GetString("sysString"));
oldDate = DateTime.FromBinary(temp);
TimeSpan difference = currentDate.Subtract(oldDate);
count = (int)(difference.TotalSeconds / waitTime);
Debug.Log("time remaining " + count);
gameRoot.AddEnergy(count);
timer = new Timer(remainingTime - (float)difference.TotalSeconds);
//PlayerPrefs.DeleteKey("TimeOnExit");
//PlayerPrefs.DeleteKey("sysString");
}
}
// Update is called once per frame
void Update()
{
if (Gameroot.isFull)
{
GetComponent<Text>().text = "Full";
count = 0;
timer.refresh();
}
else
{
//Debug.Log("deltatime ************************"+ secondstime);
timer.countDown();
if (timer.isFinished())
{
timer = new Timer(waitTime);
timer.refresh();
gameRoot.AddEnergy(1);
}
UpdateClock();
}
}
void UpdateClock()
{
int seconds = ((int)timer.timeLeft) % 60;
int minutes = (int)(timer.timeLeft / 60);
GetComponent<Text>().text = minutes.ToString() + ":" + seconds.ToString("00");
}
void OnApplicationQuit()
{
PlayerPrefs.SetFloat("TimeOnExit", timer.timeLeft);
var today = DateTime.Now;
PlayerPrefs.SetString("sysString", today.ToBinary().ToString());
PlayerPrefs.Save();
}
}
Please help what is wrong in above code or what is missing.
From OnApplicationQuit
Note: iOS applications are usually suspended and do not quit. You should tick "Exit on Suspend" in Player settings for iOS builds to cause the game to quit and not suspend, otherwise you may not see this call. If "Exit on Suspend" is not ticked then you will see calls to OnApplicationPause instead.
I suspect it is just the same with modern Android where your app is also not really closed but hibernate.
So I think you could use OnApplicationPause and OnApplicationFocus
On Android, when the on-screen keyboard is enabled, it causes a OnApplicationFocus(false) event. Additionally, if you press "Home" at the moment the keyboard is enabled, the OnApplicationFocus() event is not called, but OnApplicationPause() is called instead.
and just do the same as you do in OnApplicationQuit also for OnApplicationPause(true) and OnApplicationFocus(false)
and do the same as you do in Start also in OnApplicationPause(false) and OnApplictionFocus(true) like
public class EnergyAdder : MonoBehaviour
{
public float waitTime = 600;
public GameRoot gameRoot;
public int count;
// already reference this in the Inspector
[SerializeField] private Text timerText;
private Timer timer;
private DateTime currentDate;
private DateTime oldDate;
private void Awake()
{
// store this reference ONCE
if(!timerText) timerText = GetComponent<Text>();
}
private void Start()
{
GetTime();
}
private void Update()
{
// Note: probably a typo? shouldn't it be "gameRoot" ?
if (Gameroot.isFull)
{
timerText.text = "Full";
count = 0;
timer.refresh();
}
else
{
//Debug.Log("deltatime ************************"+ secondstime);
timer.countDown();
if (timer.isFinished())
{
timer = new Timer(waitTime);
timer.refresh();
gameRoot.AddEnergy(1);
}
UpdateClock();
}
}
private void UpdateClock()
{
int seconds = ((int)timer.timeLeft) % 60;
int minutes = (int)(timer.timeLeft / 60);
timerText.text = $"{minutes}:{seconds:00}";
}
private void OnApplicationQuit()
{
StoreTime();
}
// pauseStatus is True if the application is paused, else False.
private void OnApplicationPause(bool pauseStatus)
{
if(pauseStatus)
{
StoreTime();
}
else
{
GetTime();
}
}
// hasFocus is True if the GameObjects have focus, else False.
private void OnApplicationFocus(bool hasFocus)
{
if(hasFocus)
{
GetTime();
}
else
{
StoreTime();
}
}
private void StoreTime()
{
PlayerPrefs.SetFloat("TimeOnExit", timer.timeLeft);
var today = DateTime.Now;
PlayerPrefs.SetString("sysString", today.ToBinary().ToString());
PlayerPrefs.Save();
}
private void GetTime()
{
// NOTE: You should consider to pass a default value like
//float remainingTime = PlayerPrefs.GetFloat("TimeOnExit", 15);
float remainingTime = PlayerPrefs.GetFloat("TimeOnExit");
timer = new Timer(remainingTime);
if (PlayerPrefs.HasKey("sysString"))
{
currentDate = DateTime.Now;
long temp = Convert.ToInt64(PlayerPrefs.GetString("sysString"));
oldDate = DateTime.FromBinary(temp);
TimeSpan difference = currentDate.Subtract(oldDate);
count = (int)(difference.TotalSeconds / waitTime);
Debug.Log("time remaining " + count);
gameRoot.AddEnergy(count);
timer = new Timer(remainingTime - (float)difference.TotalSeconds);
}
}
}
Note: Typed on smartphone so I can't test it but I hope it goes into the right direction

Using a timer in conjunction with 2 push buttons from arduino

So I am using two push buttons (connected to an Arduino Uno) as an input to my game. The player has to push down both buttons at the same time for the character to move in the game. I want the player to hold down the buttons for a different amount of time in each level. I have a working Arduino and a working Unity timer and player script, but am not able to get the code to do what I want. What I basically want is that only when the player presses the buttons down, does the timer start counting down. Right now, the timer starts as soon as the scene begins. I know that I somehow have to reference the timer script to the button object, I have tried this but it still doesn't work. Note that the timer UI does have a Timer tag on it. I have also referenced the Player Controller script in the Timer script. Right now, Its giving me a range of errors. I have attached an image depicting these errors.error image
The Timer script:
using UnityEngine;
using System.Collections;
using UnityEngine.UI;
using UnityEngine.SceneManagement;
public class Timer : MonoBehaviour
{
//int startTime = 0;
public bool buttonPressed = false;
public int timeLeft;
public Text countdownText;
GameObject Character;
void Awake()
{
Character = GameObject.FindWithTag("Player");
}
public void Start()
{
//StartCoroutine("LoseTime");
BeginTimer();
}
void Update()
{
countdownText.text = ("Time Left = " + timeLeft);
if (timeLeft <= 0)
{
//StopCoroutine("LoseTime");
//countdownText.text = "Times Up!";
Invoke("ChangeLevel", 0.1f);
}
}
public void BeginTimer()
{
Character.GetComponent<PlayerController>().Update();
//gameObject.GetComponent<MyScript2>().MyFunction();
if (buttonPressed == true )
{
StartCoroutine("LoseTime");
}
else if (buttonPressed == false)
{
StopCoroutine("LoseTime");
}
}
IEnumerator LoseTime()
{
while (true)
{
yield return new WaitForSeconds(1);
timeLeft--;
}
}
void ChangeLevel()
{
SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex + 1);
}
}
The Player Script:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.IO.Ports;
public class PlayerController : MonoBehaviour
{
SerialPort sp = new SerialPort("\\\\.\\COM4", 9600);
//player == GameObject.FindWithTag("Player").GetComponent<>();
public float Speed;
public Vector2 height;
public float xMin, xMax, yMin, yMax;
public bool buttonPressed = false;
GameObject Character;
public void Awake()
{
Character = GameObject.FindWithTag("Player");
}
public void Start()
{
if (!sp.IsOpen)
{ // If the erial port is not open
sp.Open(); // Open
}
sp.ReadTimeout = 1; // Timeout for reading
}
public void Update()
{
if (sp.IsOpen)
{ // Check to see if the serial port is open
try
{
string value = sp.ReadLine();//To("Button"); //Read the information
int button = int.Parse(value);
//float amount = float.Parse(value);
//transform.Translate(Speed * Time.deltaTime, 0f, 0f); //walk
if (button == 0) //*Input.GetKeyDown(KeyCode.Space*/) //jump
{
buttonPressed = true;
Character.GetComponent<Rigidbody2D>().AddForce(height, ForceMode2D.Impulse);
Character.GetComponent<Rigidbody2D>().position = new Vector3
(
Mathf.Clamp(GetComponent<Rigidbody2D>().position.x, xMin, xMax),
Mathf.Clamp(GetComponent<Rigidbody2D>().position.y, yMin, yMax)
);
Timer tmr = GameObject.Find("Timer").GetComponent<Timer>();
tmr.BeginTimer();
}
}
catch (System.Exception)
{
}
}
void ApplicationQuit()
{
if (sp != null)
{
{
sp.Close();
}
}
}
}
}
I think the problem may be with how I am referencing the scripts in each other.
In your timer you have a quite strange mixup of Update and Coroutine. Also note that BeginTimer is called exactly once! You also shouldn't "manually" call Update of another component.
I wouldn't use Update at all here. Simply start and stop a Coroutine.
The Timer script should only do the countdown. It doesn't have to know more:
public class Timer : MonoBehaviour
{
public int timeLeft;
public Text countdownText;
private bool timerStarted;
public void BeginTimer(int seconds)
{
// Here you have to decide whether you want to restart a timer
timeLeft = seconds;
// or if you rather want to continue counting down
//if(!timerStarted) timeLeft = seconds;
StartCoroutine(LoseTime());
}
public void StopTimer()
{
StopAllCoroutines();
}
private IEnumerator LoseTime()
{
timerStarted = true;
while (timeLeft > 0)
{
yield return new WaitForSeconds(1);
timeLeft --;
countdownText.text = $"Time Left = {timeLeft}";
}
// Only reached after the timer finished and wasn't interrupted meanwhile
// Using Invoke here is a very good idea since we don't want to interrupt anymore
// if the user lets go of the button(s) now
Invoke(nameof(ChangeLevel), 0.1f);
}
void ChangeLevel()
{
SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex + 1);
}
}
In general avoid to use Find at all. If anyhow possible already reference things in the Inspector! If needed you can use Find but only once! What you never want to do is use any of the Find and GetComponent variants repeatedly - rather store the reference the first time and re-use it - and especially not in Update no a per frame basis. They are very expensive calls!
public class PlayerController : MonoBehaviour
{
public float Speed;
public Vector2 height;
// I prefer to use Vector2 for such things
public Vector2 Min;
public Vector2 Max;
public bool buttonPressed = false;
// Already reference these via the Inspector if possible!
public Rigidbody2D Character;
public Timer timer;
public Rigidbody2D _rigidbody;
private SerialPort sp = new SerialPort("\\\\.\\COM4", 9600);
private void Awake()
{
FetchReferences();
}
// This is my "secret" tip for you! Go to the component in the Inspector
// open the ContextMenu and hit FetchReferences
// This already stores the references in the according fields ;)
[ContextMenu("FetchReferences")]
private void FetchReferences()
{
if(!Character)Character = GameObject.FindWithTag("Player"). GetComponent<Rigidbody2D>();
if(!timer) timer = GameObject.Find("Timer").GetComponent<Timer>();
}
private void Start()
{
if (!sp.IsOpen)
{
sp.Open(); // Open
}
sp.ReadTimeout = 1;
}
private void Update()
{
// I wouldn't do the serialport open check here
// your if block simply silently hides the fact that your application
// doesn't work correctly! Rather throw an error!
try
{
string value = sp.ReadLine(); //Read the information
int button = int.Parse(value);
//TODO: Since it isn't clear in your question how you get TWO buttons
//TODO: You will have to change this condition in order to only fire if both
//TODO: buttons are currently pressed!
buttonPressed = button == 0;
if (buttonPressed)
{
Character.AddForce(height, ForceMode2D.Impulse);
// The clamping of a rigidbody should always be done ine FixedUpdate!
// Pass in how many seconds as parameter or make the method
// parameterless and configure a fixed duration via the Inspector of the Timer
timer.BeginTimer(3.0f);
}
else
{
// Maybe stop the timer if condition is not fulfilled ?
timer.StopTimer();
}
}
catch (System.Exception)
{
// You should do something here! At least a Log ...
}
}
private void FixedUpdate()
{
// Here I wasn't sure: Are there actually two different
// Rigidbody2D involved? I would assume you rather wanted to use the Character rigidbody again!
Character.position = new Vector3(Mathf.Clamp(Character.position.x, Min.x, Max.x), Mathf.Clamp(Character.position.y, Min.y, Max.y));
}
// Did you mean OnApplicationQuit here?
private void ApplicationQuit()
{
if (sp != null)
{
{
sp.Close();
}
}
}
}
Typed on smartphone but I hope the idea gets clear

Coroutine still running even if game is paused and time scale set to 0

As the title pointed out, for some reason when my game is paused my co-routine still runs. I even went as far as to put the time scale condition in a while condition so that the while doesnt run if it paused but to no avail. I've added my code in it's entirety and hope that someone will be able to assist.
using UnityEngine;
using System.Collections;
using Chronos;
public class ObjectSpawn : BaseBehaviour //MonoBehaviour
{
public float minTime = 3f;
public float maxTime = 9f;
public float minX = -65.5f;
public float maxX = -5.5f;
public float topY = -5.5f;
public float z = 0.0f;
public int count = 50;
public GameObject prefab;
public bool doSpawn = true;
public float fallGrav =1.0f;
int first = 1;
void Start()
{
Clock clock = Timekeeper.instance.Clock("MovingOneWayPlatforms");
StartCoroutine(Spawner());
}
IEnumerator Spawner()
{
while (first == 1) {
yield return time.WaitForSeconds(8.0f);
first = 0;
}
while (doSpawn && count > 0 /*&& time.timeScale != 0 */)
{
Renderer renderer = GetComponent<Renderer>();
float min = renderer.bounds.min.x;
float max = renderer.bounds.max.x;
Vector3 v12 = new Vector3(Random.Range(minX, maxX), this.gameObject.transform.position.y, 0f);
prefab.GetComponent<Rigidbody2D>().gravityScale = fallGrav;
prefab = Instantiate(prefab, v12, Random.rotation);
count--;
// yield return new WaitForSeconds(Random.Range(minTime, maxTime));
yield return time.WaitForSeconds(Random.Range(minTime, maxTime));
Destroy(prefab, 6);
}
}
}
Try to uncomment your 2nd while statement, I think that is your problem.
I am new and still not used to Chronos.
Maybe I'm wrong but my guess is this line.
Destroy(prefab, 6);
In my understanding, Destroy's delay should not affected by chronos.
You better use new Coroutine to destroy it.
like this
StartCoroutine(DestroyRoutine(prefab))
IEnumurator DestroyRoutine(GameObject gameobject)
{
yield return time.WaitForSeconds(6);
Destroy(gameObject)
}

What I Have Learned About Unity ScrollRect / ScrollView Optimization / Performance

ScrollView performance is a real drag (get it?), especially on mobile platforms. I frequently found myself getting less that 15 fps which left the user experience feeling jolting and underwhelming. After a lot of research and testing I have compiled a checklist to drastically improve performance. I now get at least 30 fps, with the majority of the CPU time allocated to WaitForTargetFPS.
I hope this helps anyone who is also having trouble in this area. Optimization solutions are hard to come by. Feel free to use and modify any of my code here.
ONE:
.GetComponent<>() calls are inefficient, especially outside the editor. Avoid using these in any kind of Update() method.
TWO:
OnValueChanged() is called every frame that the ScrollView is being dragged. It is therefore in a sense equivalent to Update() so you should avoid using .GetComponent<>() calls in this method.
THREE:
Whenever any element on a Canvas is changed the entire Canvas must rebuild its batches. This operation can be very expensive. It is therefore recommended to split your UI elements across at least two Canvases, one for elements that are changed rarely or never and one elements that change often.
Whenever a ScrollView scrolls the entire Canvas it is on is dirtied. It is therefore recommended that you have each ScrollView on a separate Canvas.
Unity Canvas Rebuilds Explanation: https://unity3d.com/learn/tutorials/topics/best-practices/fill-rate-canvases-and-input?playlist=30089
FOUR:
EventSystem.Update() handles the input detection in a scene, using raycasts to filter through the hierarchy in order to find a component to accept this input. Therefore these calculations are only done when interacting with the scene, like when a ScrollView is being scrolled. Removing unnecessary RaycastTarget properties from graphics and text will improve this processing time. It mightn't make a big difference, but if you're not careful enough objects can make the input handling time really add up.
FIVE:
With any kind of mask component, even a RectMask2D, all objects in a ScrollView are batched and rendered. If you have a lot of elements in your ScrollView, it is recommended that you utilize some kind of pooling solution. There are many of these available on the app store.
Unity Pooling Explanation: https://unity3d.com/learn/tutorials/topics/best-practices/optimizing-ui-controls
If, however, your project is incompatible with this, needing persistent elements, I would recommend that you hide your off screen objects to reduce performance overhead. Transform.SetParent() and GameObject.SetActive() are both resource intensive methods, instead attach a CanvasGroup component to each element and adjust the alpha value to achieve the same effect.
Here is a static script to detect if an object is visible or not and to set the alpha accordingly:
using UnityEngine;
using UnityEngine.UI;
public class ScrollHider : MonoBehaviour {
static public float contentTop;
static public float contentBottom;
static public bool HideObject(GameObject givenObject, CanvasGroup canvasGroup, float givenPosition, float givenHeight) {
if ((Mathf.Abs(givenPosition) + givenHeight > contentTop && Mathf.Abs(givenPosition) + givenHeight < contentBottom) || (Mathf.Abs(givenPosition) > contentTop && Mathf.Abs(givenPosition) < contentBottom)) {
if (canvasGroup.alpha != 1) {
canvasGroup.alpha = 1;
}
return true;
} else {
if (canvasGroup.alpha != 0) {
canvasGroup.alpha = 0;
}
return false;
}
}
static public void Setup(Scroll givenScroll) {
contentTop = (1 - givenScroll.verticalNormalizedPosition) * (givenScroll.content.rect.height - givenScroll.viewport.rect.height);
contentBottom = contentTop + givenScroll.viewport.rect.height;
}
}
SIX:
Unity's built in ScrollRect component allows for broad, modular functionality. However, in terms of performance it can be noticeably slower than if you were to write your own. Here is a Scroll script that achieves the same ends, but only supports the vertical, clamped and inertia properties of Unity's ScrollRect.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.EventSystems;
public class Scroll : MonoBehaviour, IPointerDownHandler, IPointerUpHandler, IDragHandler, IScrollHandler {
private Camera mainCamera;
private RectTransform canvasRect;
public RectTransform viewport;
public RectTransform content;
private Rect viewportOld;
private Rect contentOld;
private List<Vector2> dragCoordinates = new List<Vector2>();
private List<float> offsets = new List<float>();
private int offsetsAveraged = 4;
private float offset;
private float velocity = 0;
private bool changesMade = false;
public float decelration = 0.135f;
public float scrollSensitivity;
public OnValueChanged onValueChanged;
[System.Serializable]
public class OnValueChanged : UnityEvent { }
[HideInInspector]
public float verticalNormalizedPosition
{
get
{
float sizeDelta = CaculateDeltaSize();
if (sizeDelta == 0) {
return 0;
} else {
return 1 - content.transform.localPosition.y / sizeDelta;
}
}
set
{
float o_verticalNormalizedPosition = verticalNormalizedPosition;
float m_verticalNormalizedPosition = Mathf.Max(0, Mathf.Min(1, value));
float maxY = CaculateDeltaSize();
content.transform.localPosition = new Vector3(content.transform.localPosition.x, Mathf.Max(0, (1 - m_verticalNormalizedPosition) * maxY), content.transform.localPosition.z);
float n_verticalNormalizedPosition = verticalNormalizedPosition;
if (o_verticalNormalizedPosition != n_verticalNormalizedPosition) {
onValueChanged.Invoke();
}
}
}
private float CaculateDeltaSize() {
return Mathf.Max(0, content.rect.height - viewport.rect.height); ;
}
private void Awake() {
mainCamera = GameObject.Find("Main Camera").GetComponent<Camera>();
canvasRect = transform.root.GetComponent<RectTransform>();
}
private Vector2 ConvertEventDataDrag(PointerEventData eventData) {
return new Vector2(eventData.position.x / mainCamera.pixelWidth * canvasRect.rect.width, eventData.position.y / mainCamera.pixelHeight * canvasRect.rect.height);
}
private Vector2 ConvertEventDataScroll(PointerEventData eventData) {
return new Vector2(eventData.scrollDelta.x / mainCamera.pixelWidth * canvasRect.rect.width, eventData.scrollDelta.y / mainCamera.pixelHeight * canvasRect.rect.height) * scrollSensitivity;
}
public void OnPointerDown(PointerEventData eventData) {
velocity = 0;
dragCoordinates.Clear();
offsets.Clear();
dragCoordinates.Add(ConvertEventDataDrag(eventData));
}
public void OnScroll(PointerEventData eventData) {
UpdateOffsetsScroll(ConvertEventDataScroll(eventData));
OffsetContent(offsets[offsets.Count - 1]);
}
public void OnDrag(PointerEventData eventData) {
dragCoordinates.Add(ConvertEventDataDrag(eventData));
UpdateOffsetsDrag();
OffsetContent(offsets[offsets.Count - 1]);
}
public void OnPointerUp(PointerEventData eventData) {
dragCoordinates.Add(ConvertEventDataDrag(eventData));
UpdateOffsetsDrag();
OffsetContent(offsets[offsets.Count - 1]);
float totalOffsets = 0;
foreach (float offset in offsets) {
totalOffsets += offset;
}
velocity = totalOffsets / offsetsAveraged;
dragCoordinates.Clear();
offsets.Clear();
}
private void OffsetContent(float givenOffset) {
float newY = Mathf.Max(0, Mathf.Min(CaculateDeltaSize(), content.transform.localPosition.y + givenOffset));
if (content.transform.localPosition.y != newY) {
content.transform.localPosition = new Vector3(content.transform.localPosition.x, newY, content.transform.localPosition.z);
}
onValueChanged.Invoke();
}
private void UpdateOffsetsDrag() {
offsets.Add(dragCoordinates[dragCoordinates.Count - 1].y - dragCoordinates[dragCoordinates.Count - 2].y);
if (offsets.Count > offsetsAveraged) {
offsets.RemoveAt(0);
}
}
private void UpdateOffsetsScroll(Vector2 givenScrollDelta) {
offsets.Add(givenScrollDelta.y);
if (offsets.Count > offsetsAveraged) {
offsets.RemoveAt(0);
}
}
private void LateUpdate() {
if (viewport.rect != viewportOld) {
changesMade = true;
viewportOld = new Rect(viewport.rect);
}
if (content.rect != contentOld) {
changesMade = true;
contentOld = new Rect(content.rect);
}
if (velocity != 0) {
changesMade = true;
velocity = (velocity / Mathf.Abs(velocity)) * Mathf.FloorToInt(Mathf.Abs(velocity) * (1 - decelration));
offset = velocity;
}
if (changesMade) {
OffsetContent(offset);
changesMade = false;
offset = 0;
}
}
}
A nice article explains that the default targetFrameRate might be responsible for the unsmooth scrolling behavior of the scrollView. This can be resolved via:
Application.targetFrameRate = 60; // or whatever you wish. 60 turned out enough for us
Of course this setting is only effective if you have resolved the performance issues (as it is nicely explained by Phedg1).