【问题标题】:Optimizing sprite change for game object with multiple children优化具有多个孩子的游戏对象的精灵变化
【发布时间】:2021-10-10 09:17:42
【问题描述】:

我有一个这样的对象结构:

我必须用几种不同的设备创建一个玩家。为此,我使用了一个自定义动画控制器,它更改了一个索引,该索引将确定几个精灵表中的精灵:

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;
    }
  }
}

所以在这里我运行 update 函数设定的等待时间,并根据来自 HeroMovement 脚本的变量更新布尔值以确定如何根据声明的数组更新索引:

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;
  // }
}

每个子游戏对象还使用currentHeroSpriteIndex 来更新它们的精灵、基于序列化字段的颜色以及基于父对象的位置:

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;
  }
}

为此,我使用另一个脚本 HeroResources 加载字典中的所有精灵:

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");
  }
}

因此,澄清一下:Hero 游戏对象附加了脚本 AnimationControllerHeroMovementHeroResources,而子游戏对象仅附加了 SpritePosition 脚本。

想法是加载精灵,然后根据运动脚本中的逻辑,决定动画师使用的布尔值,检查哪个是活动的(目前我只有running工作),然后根据哪个一个是 true 确定要使用的精灵索引。所有精灵都具有相同的名称和尺寸,这就是为什么单个索引可以改变所有精灵的原因。我基本上这样做是为了避免为每种不同的设备使用数百个动画。

所以,虽然精灵以这种方式同步更新,但我不确定我是否没有优化精灵更改(和重绘颜色)过程,因为当我按箭头键运行时,我得到以下信息:

虽然我使用的屏幕录像机似乎减慢了播放速度,但有时会通过裤子(有时是靴子)看到英雄的身体,尽管精灵不应该重叠。

我不确定这是否是因为计算机的内存,但有 14 个精灵用于跑步运动,在动画持续的 2 秒左右的跨度内总共使用了 84 个精灵。是我使用太多内存来加载所需的精灵吗?我是否应该尝试找到一种方法来合并精灵以只有一个孩子的精灵得到更新?如果有人对如何提高我的代码性能有任何建议,请告诉我。

编辑:

我选择更改我的代码以取消 HeroResources.cs 脚本,该脚本最终会在开始时加载每个可用资产,而是将资源加载包含在 SpritePosition.cs 中,以便每个子游戏对象处理自己的 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;
  }
}

但是,这似乎并没有提高性能。我已经使用深度分析检查了分析器,虽然我看到了很大的尖峰,但与 EditorLoopPlayerLoop 相比,我没有看到来自 SetSprite 函数的垃圾,也没有太大的延迟:

我认为改变精灵的函数被调用的频率会有问题,或者随着精灵的不断变化会产生多少垃圾,但似乎这可能不是问题。

【问题讨论】:

    标签: c# unity3d


    【解决方案1】:

    如果您想发现延迟或高内存使用的来源,请使用统一分析器,如果可能,请使用第 3 方分析器,因为统一有点不好,也可以使用深度分析(统一将分析每个方法调用,很好查看导致延迟的更改,请记住,使用分析器会降低性能并增加内存使用量,只要您启用它)。您需要改进很多东西,但我能想到的所有问题都将通过深入分析变得显而易见。

    【讨论】:

    • 谢谢。我将考虑为此使用分析器。如果我偶然发现需要更改的任何代码,会通知您
    • 我修改了我的代码以从子游戏对象的脚本加载以尝试仅加载当前使用的资源,并尝试了解分析器显示的内容,但我不确定可能是什么如果几乎没有延迟也没有垃圾收集,则会导致延迟。如果可以,请您查看编辑并告诉我是否检查了探查器错误或是否应该尝试其他方法?
    猜你喜欢
    • 2012-08-08
    • 1970-01-01
    • 2014-09-17
    • 1970-01-01
    • 2020-09-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多