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

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).

Related

Does Unity have a built in function that is triggered when Screen.height or Screen.width change?

I have build some custom UI scaling features for a mobile app built in Unity to compensate for various phone screen ratios. I would also like protection against screen size changes, as with the rise of foldable phones, I presume that is a risk.
The preliminary solution I have implemented is to have a script attached to the objects that must potentially resize, structured like this:
public class ScreenElementSizer : MonoBehaviour {
private int screenWidth_1 = Screen.width;
private int screenHeight_1 = Screen.height;
// Start is called before the first frame update
void Start() {
ScreenResized();
}
// Resizing functions here
private void ScreenResized() {
}
// On every screen refresh
void Update()
{
if ((Screen.width != screenWidth_1) || (Screen.height != screenHeight_1)) {
ScreenResized();
screenWidth_1 = Screen.width;
screenHeight_1 = Screen.height;
}
}
As you can see it's a pretty simple solution where I'm storing the prior sample's Screen.width and Screen.height in variables (screenWidth_1 & screenHeight_1), then comparing them on each sample (update) and if there is a discrepancy (screen change) trigger the resizer script.
It works fine of course. I think this is pretty standard coding logic. It's probably not expensive to run one/two extra if statements like this per object per sample.
I'm just new to Unity and wondering if there's any better, built in, or more efficient way to do this task.
To be clear, I am aware of the built in canvas resizer tools that scale based on width and height. I specifically am referring to the case where you want to apply some special script based resizing logic like this when the screen size changes.
Thanks for any thoughts.
There is no built-in event, but you can make your own event.
DeviceChange.cs -
using System;
using System.Collections;
using UnityEngine;
public class DeviceChange : MonoBehaviour
{
public static event Action<Vector2> OnResolutionChange;
public static event Action<DeviceOrientation> OnOrientationChange;
public static float CheckDelay = 0.5f; // How long to wait until we check again.
static Vector2 resolution; // Current Resolution
static DeviceOrientation orientation; // Current Device Orientation
static bool isAlive = true; // Keep this script running?
void Start() {
StartCoroutine(CheckForChange());
}
IEnumerator CheckForChange(){
resolution = new Vector2(Screen.width, Screen.height);
orientation = Input.deviceOrientation;
while (isAlive) {
// Check for a Resolution Change
if (resolution.x != Screen.width || resolution.y != Screen.height ) {
resolution = new Vector2(Screen.width, Screen.height);
if (OnResolutionChange != null) OnResolutionChange(resolution);
}
// Check for an Orientation Change
switch (Input.deviceOrientation) {
case DeviceOrientation.Unknown: // Ignore
case DeviceOrientation.FaceUp: // Ignore
case DeviceOrientation.FaceDown: // Ignore
break;
default:
if (orientation != Input.deviceOrientation) {
orientation = Input.deviceOrientation;
if (OnOrientationChange != null) OnOrientationChange(orientation);
}
break;
}
yield return new WaitForSeconds(CheckDelay);
}
}
void OnDestroy(){
isAlive = false;
}
}
Implementation -
DeviceChange.OnOrientationChange += MyOrientationChangeCode;
DeviceChange.OnResolutionChange += MyResolutionChangeCode;
DeviceChange.OnResolutionChange += MyOtherResolutionChangeCode;
void MyOrientationChangeCode(DeviceOrientation orientation)
{
}
void MyResolutionChangeCode(Vector2 resolution)
{
}
Credit - DougMcFarlane

Unity - No Found Solution to Child Scale Inheritance

Fairly new to Unity and have tried several solutions without success. I have read so many forum posts but none are working for me.
I have been attempting to snap game objects to other game objects at runtime (such as to equip ship parts to a ship). The script works as intended but the child objects continue to be scaled with the parent object.
I have tried attaching scaling scripts that dynamically scale the child object based on the parent object scale. I've tried stepping through the logical way one would go about doing this. I have tried hierarchal fixes using empty game objects and nothing seems to be maintaining a constant global scale for my child objects. Maybe someone can see something I am missing.
Please ignore other issues that may be in the code at this point unless they directly are affecting the scaling issue. But suggestions are welcome. Interested in learning all I can.
-Hierarchy-
Ship (scale (2,2,2)) parent of the parent
-Empty Object (scale (0.5,0.5,0.5)) this is the parent set to the same scale as the snappable object
public class Interactable : MonoBehaviour
{
public Transform pickUpDestination;
public Transform snapDestination;
public Vector3 snapPointPos;
private bool isSnapped;
public bool isClicked;
public float unsnapMouseDist = 0.5f;
public float distCheck;
private void FixedUpdate()
{
distCheck = Vector3.Distance(pickUpDestination.position, snapPointPos);
if (distCheck >= unsnapMouseDist && isClicked)
{
isSnapped = false;
transform.SetParent(pickUpDestination);
transform.position = transform.parent.position;
Debug.Log("UNSNAPPED");
}
else if (distCheck <= unsnapMouseDist && isClicked)
{
isSnapped = true;
transform.SetParent(snapDestination, true);
transform.position = transform.parent.position;
Debug.Log("SNAPPED!");
}
if (transform.parent != null)
{
Debug.Log("Parent =" + transform.parent.name);
}
Debug.Log("Global Scale = " + transform.lossyScale);
Debug.Log("Local Scale = " + transform.localScale);
}
void OnMouseDown()
{
isClicked = true;
Debug.Log("Clicked = " + isClicked);
if (isSnapped == false)
{
GetComponent<Rigidbody>().useGravity = false;
GetComponent<Rigidbody>().isKinematic = true;
transform.SetParent(pickUpDestination);
}
}
void OnTriggerEnter(Collider snap)
{
if (snap.gameObject.CompareTag("Snap Point"))
{
snapPointPos = snap.transform.position;
snapDestination = snap.transform;
transform.rotation = snap.transform.rotation;
}
}
void OnMouseUp()
{
isClicked = false;
Debug.Log("Clicked = " + isClicked);
if (isSnapped == false)
{
this.transform.parent = null;
GetComponent<Rigidbody>().isKinematic = false;
GetComponent<Rigidbody>().useGravity = true;
}
}
}
And the Scaling Script:
public class Scaler : MonoBehaviour
{
public Transform parent;
private void FixedUpdate()
{
parent = transform.parent;
transform.localScale = new Vector3(transform.localScale.x / parent.localScale.x, transform.localScale.y / parent.localScale.y, transform.localScale.z / parent.localScale.z);
}
}
lossyScale is the combined effects of all parent scales on that object (some caveats but not likely to apply here).
localScale is the scale effect of only that object (the numbers you see in the transform inspector)
var trueScale == new Vector3(
desiredScale.x / parentTransform.lossyScale.x,
desiredScale.y / parentTransform.lossyScale.y,
desiredScale.z / parentTransform.lossyScale.z);
transform.localScale = trueScale;

Unity: Rotation sync over network on dedicated server

This was probably asked a dozen of times, but I can't seem to find the answer anywhere.
I have a dedicated server for a space game written in C# console application. The problems that I'm facing is the synchronisation of GameObject rotations. I'm suspecting the issue being related to gimbal lock, but I am not sure.
Here I have my player movement/rotation controller:
public class PlayerMovement : MonoBehaviour {
[SerializeField] float maxSpeed = 40f;
[SerializeField] float shipRotationSpeed = 60f;
Transform characterTransform;
void Awake()
{
characterTransform = transform;
}
void Update()
{
Turn();
Thrust();
}
float Speed() {
return maxSpeed;
}
void Turn() {
float rotX = shipRotationSpeed * Time.deltaTime * CrossPlatformInputManager.GetAxis("Vertical");
float rotY = shipRotationSpeed * Time.deltaTime * CrossPlatformInputManager.GetAxis("Horizontal");
characterTransform.Rotate(-rotX, rotY, 0);
}
void Thrust() {
if (CrossPlatformInputManager.GetAxis("Move") > 0) {
characterTransform.position += shipTransform.forward * Speed() * Time.deltaTime * CrossPlatformInputManager.GetAxis("Move");
}
}
}
This script is applied to my character object which is a ship. Note that the character object has a child object which is the ship itself and has fixed rotation and position that do not change. When character has moved/rotated I send the following to the server: position(x, y, z) and rotation(x, y, z, w).
Now here is the actual script that receives network packet information and updates the other players in game:
public class CharacterObject : MonoBehaviour {
[SerializeField] GameObject shipModel;
public int guid;
public int characterId;
public string name;
public int shipId;
Vector3 realPosition;
Quaternion realRotation;
public void Awake() {
}
public int Guid { get { return guid; } }
public int CharacterId { get { return characterId; } }
void Start () {
realPosition = transform.position;
realRotation = transform.rotation;
}
void Update () {
// Do nothing
}
internal void LoadCharacter(SNCharacterUpdatePacket cuPacket) {
guid = cuPacket.CharacterGuid;
characterId = cuPacket.CharacterId;
name = cuPacket.CharacterName;
shipId = cuPacket.ShipId;
realPosition = new Vector3(cuPacket.ShipPosX, cuPacket.ShipPosY, cuPacket.ShipPosZ);
realRotation = new Quaternion(cuPacket.ShipRotX, cuPacket.ShipRotY, cuPacket.ShipRotZ, cuPacket.ShipRotW);
UpdateTransform();
Instantiate(Resources.Load("Ships/Ship1/Ship1"), shipModel.transform);
}
internal void UpdateCharacter(SNCharacterUpdatePacket cuPacket) {
realPosition = new Vector3(cuPacket.ShipPosX, cuPacket.ShipPosY, cuPacket.ShipPosZ);
realRotation = new Quaternion(cuPacket.ShipRotX, cuPacket.ShipRotY, cuPacket.ShipRotZ, cuPacket.ShipRotW);
UpdateTransform();
}
void UpdateTransform() {
transform.position = Vector3.Lerp(transform.position, realPosition, 0.1f);
transform.rotation = Quaternion.Lerp(transform.rotation, realRotation, 0.5f);
}
}
Do you see anything wrong with the code ?
My experience with 2 players in game is the following:
When I start the game with two players they spawn at the same location (0,0,0) and same rotation (0,0,0).
Lets say that The other player is rotating continuously around the X-axis only. What I am experiencing is:
First 90 deg. of rotation it renders fine.
Next 180 deg. of rotation the object stays in place
The last 90 deg. of rotation renders fine
In the first version I did not send the 'w' value of Quaternion, but then added it in the second version and it did not help. Note that there is no restrictions in rotation and users can rotate endlessly in all directions.
Btw the positioning works fine.
I might not understand Quaternions fully, but I thought that they had a purpose in avoiding the gimbal lock issue.
Any help would be appreciated.
To answer my own question. I used transform.localEulerAngles instead of transform.rotation, and everything seems to work just fine.
Now the only thing that I am unsure is if gimbal lock could happen with this change.

Assign number to object in Unity 3D

I want to assign a number to objects in Unity 3D and the number should appear on the object. Please, I need help on a code to do this. I have little knowledge of C#, but I have been learning aggressively these days. I will appreciate any help. Thanks. Below is a sample code I assign to each of the object.
public class NumberHolder : MonoBehaviour
{
int myNumber = 0;
UnityEngine.UI.Text myTextField;
void Awake()
{
myNumber = UnityEngine.Random.Range(0,100) + 1;
myTextField = GetComponent<UnityEngine.UI.Text>();
}
void Start()
{
if(myTextField != null)
{
myTextField.text = "" + myNumber;
}
}
}
Try the script below. It should contain some tidbits to direct you. A couple things to note: for this script, your object must have a collider (doesn't matter which kind: 2D or 3D). Also, this script displays the object's number only when the mouse is over the object. I could tailor it to always show the object, but I gotta make you work a little. Plus, I think in the long run this will work splendidly for you, as you can dissect it.
I will give you a hint. What's the secret to drawing text at the object's position?
You need to convert the object's world space coordinates, to screen space coordinates and then overlay your text.
Consider the following code blocks.
These methods determine if the mouseposition on screen is over the object:
private void OnMouseEnter()
{
isVis = true;
}
private void OnMouseExit()
{
isVis = false;
}
These lines draw the objects number at the mouse location:
Vector2 mospos = Input.mousePosition;
mospos.y = Screen.height - mospos.y;
if (isVis)
{
GUI.Box(new Rect(Input.mousePosition.x + 2, Screen.height -
Input.mousePosition.y + 2, 128, 72),"" + myNumber);
}
Full class below. Hope it helps!
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class LocalLabelingScript : MonoBehaviour {
Monster beast;
bool isVis = false;
int myNumber;
private void Awake()
{
myNumber = UnityEngine.Random.Range(0,100) + 1;
}
private void OnGUI()
{
Vector2 mospos = Input.mousePosition;
mospos.y = Screen.height - mospos.y;
if (isVis)
{
GUI.Box(new Rect(Input.mousePosition.x + 2, Screen.height -
Input.mousePosition.y + 2, 128, 72),"" + myNumber);
}
private void OnMouseEnter()
{
isVis = true;
}
private void OnMouseExit()
{
isVis = false;
}
}

want to get screen point on render texture camera unity3d

I am making sniper game, I have 2 cameras, one is rendering whole environment, and second one is in scope of the gun, which has high zoom quality.
I am able to show, enemy point on screen through main camera. and it shows proper on main camera, but in scope camera(2nd camera), it not showing properly. I want to show these points exactly same as enemy is.
here is my script to show point so far. I am attaching screen shots, first one is for scope camera and does not showing proper place, and 2nd one is for main camera, showing at proper place (Red circle is pointing)
using UnityEngine;
using UnityEngine.UI;
public class identity_shower : MonoBehaviour {
public Texture tex;
public GameObject helt;
public bool target_is_high =false;
private int counter_for_high = -1;
private int counter_for_low = -1;
private int counter_value = 50;
private bool bool_for_high = false;
private bool bool_for_low = false;
private Camera scope_cam_active;
private RectTransform rectTransform;
private Vector2 uiOFFset;
// Update is called once per frame
void OnGUI()
{
GameObject[] objs = GameObject.FindGameObjectsWithTag("enemy_soldier");
if( objs.Length<=0 )
{
return;
}
if (bool_for_high)
{
Vector2 pos_to_display = Camera.main.WorldToScreenPoint (this.transform.position);
GUI.DrawTexture (new Rect(pos_to_display.x-10.0f,(Screen.height- pos_to_display.y)-40.0f,15,15),tex);
}
}
public void show_your_identity()
{
if (target_is_high) {
if (counter_for_high >= counter_value)
{
return;
}
counter_for_high = counter_for_high + 1;
if (counter_for_high >= counter_value)
{
bool_for_high = true;
}
} else if(!target_is_high)
{
if (counter_for_low >= counter_value)
{
return;
}
counter_for_low = counter_for_low + 1;
if (counter_for_low >= counter_value)
{
bool_for_low = true;
}
}
}
}