diff --git a/Assets/Scripts/PlayerController.cs b/Assets/Scripts/PlayerController.cs index 5e86666..96b1ed3 100644 --- a/Assets/Scripts/PlayerController.cs +++ b/Assets/Scripts/PlayerController.cs @@ -1,123 +1,418 @@ +using System.Collections; using UnityEngine; using UnityEngine.InputSystem; public class PlayerMovement : MonoBehaviour { - private Rigidbody2D rb; + //Scriptable object which holds all the player's movement parameters. If you don't want to use it + //just paste in all the parameters, though you will need to manuly change all references in this script - public float maxRunSpeed; - public float runAcceleration; - public float snappiness = 1; - public float jumpSpeed; - [Range(0,1)] public float airSpeedMultiplier; - private bool onGround = false; - private float forward = 1; + //HOW TO: to add the scriptable object, right-click in the project window -> create -> Player Data + //Next, drag it into the slot in playerMovement on your player - float hangTimeThreshold = 0.1f; - float hangTimeAccel = 0; - float hangTimeSpeed = 0; + public PlayerData Data; - private Vector2 movement = Vector2.zero; + #region Variables + //Components + public Rigidbody2D RB { get; private set; } - public LayerMask groundLayer; - public Vector2 boxSize; - public float maxDistanceFromGround; + //Variables control the various actions the player can perform at any time. + //These are fields which can are public allowing for other sctipts to read them + //but can only be privately written to. + public bool IsFacingRight { get; private set; } + public bool IsJumping { get; private set; } + public bool IsWallJumping { get; private set; } + public bool IsSliding { get; private set; } - [Header("State Control:")] - [SerializeField] private StateController stateController; + //Timers (also all fields, could be private and a method returning a bool could be used) + public float LastOnGroundTime { get; private set; } + public float LastOnWallTime { get; private set; } + public float LastOnWallRightTime { get; private set; } + public float LastOnWallLeftTime { get; private set; } - PlayerBehavior playerBehavior; + //Jump + private bool _isJumpCut; + private bool _isJumpFalling; - void OnValidate() + //Wall Jump + private float _wallJumpStartTime; + private int _lastWallJumpDir; + + private Vector2 _moveInput; + public float LastPressedJumpTime { get; private set; } + + //Set all of these up in the inspector + [Header("Checks")] + [SerializeField] private Transform _groundCheckPoint; + //Size of groundCheck depends on the size of your character generally you want them slightly small than width (for ground) and height (for the wall check) + [SerializeField] private Vector2 _groundCheckSize = new Vector2(0.49f, 0.03f); + [Space(5)] + [SerializeField] private Transform _frontWallCheckPoint; + [SerializeField] private Transform _backWallCheckPoint; + [SerializeField] private Vector2 _wallCheckSize = new Vector2(0.5f, 1f); + + [Header("Layers & Tags")] + [SerializeField] private LayerMask _groundLayer; + #endregion + + private void Awake() { - this.runAcceleration = Mathf.Clamp(runAcceleration, 0.1f, this.maxRunSpeed); + RB = GetComponent(); } - void Start() + private void Start() { - playerBehavior = this.gameObject.GetComponent(); - this.rb = this.GetComponent(); - stateController = GameObject.Find("StateController").GetComponent(); + SetGravityScale(Data.gravityScale); + IsFacingRight = true; } void OnMove(InputValue value) { - this.movement = value.Get(); - //Debug.Log(this.movement); + this._moveInput = value.Get(); } - void OnJump() { - if (IsGrounded()) { - rb.AddForce(Vector2.up * jumpSpeed, ForceMode2D.Impulse); + void OnJump() + { + OnJumpInput(); + } + + private void Update() + { + #region TIMERS + LastOnGroundTime -= Time.deltaTime; + LastOnWallTime -= Time.deltaTime; + LastOnWallRightTime -= Time.deltaTime; + LastOnWallLeftTime -= Time.deltaTime; + + LastPressedJumpTime -= Time.deltaTime; + #endregion + + #region INPUT HANDLER + + if (_moveInput.x != 0) + CheckDirectionToFace(_moveInput.x > 0); + #endregion + + #region COLLISION CHECKS + if (!IsJumping) + { + //Ground Check + if (Physics2D.OverlapBox(_groundCheckPoint.position, _groundCheckSize, 0, _groundLayer) && !IsJumping) //checks if set box overlaps with ground + { + LastOnGroundTime = Data.coyoteTime; //if so sets the lastGrounded to coyoteTime + } + + //Right Wall Check + if (((Physics2D.OverlapBox(_frontWallCheckPoint.position, _wallCheckSize, 0, _groundLayer) && IsFacingRight) + || (Physics2D.OverlapBox(_backWallCheckPoint.position, _wallCheckSize, 0, _groundLayer) && !IsFacingRight)) && !IsWallJumping) + LastOnWallRightTime = Data.coyoteTime; + + //Right Wall Check + if (((Physics2D.OverlapBox(_frontWallCheckPoint.position, _wallCheckSize, 0, _groundLayer) && !IsFacingRight) + || (Physics2D.OverlapBox(_backWallCheckPoint.position, _wallCheckSize, 0, _groundLayer) && IsFacingRight)) && !IsWallJumping) + LastOnWallLeftTime = Data.coyoteTime; + + //Two checks needed for both left and right walls since whenever the play turns the wall checkPoints swap sides + LastOnWallTime = Mathf.Max(LastOnWallLeftTime, LastOnWallRightTime); } + #endregion + + #region JUMP CHECKS + if (IsJumping && RB.velocity.y < 0) + { + IsJumping = false; + + if (!IsWallJumping) + _isJumpFalling = true; + } + + if (IsWallJumping && Time.time - _wallJumpStartTime > Data.wallJumpTime) + { + IsWallJumping = false; + } + + if (LastOnGroundTime > 0 && !IsJumping && !IsWallJumping) + { + _isJumpCut = false; + + if (!IsJumping) + _isJumpFalling = false; + } + + //Jump + if (CanJump() && LastPressedJumpTime > 0) + { + IsJumping = true; + IsWallJumping = false; + _isJumpCut = false; + _isJumpFalling = false; + Jump(); + } + //WALL JUMP + // else if (CanWallJump() && LastPressedJumpTime > 0) + // { + // IsWallJumping = true; + // IsJumping = false; + // _isJumpCut = false; + // _isJumpFalling = false; + // _wallJumpStartTime = Time.time; + // _lastWallJumpDir = (LastOnWallRightTime > 0) ? -1 : 1; + // + // WallJump(_lastWallJumpDir); + // } + #endregion + + #region SLIDE CHECKS + if (CanSlide() && ((LastOnWallLeftTime > 0 && _moveInput.x < 0) || (LastOnWallRightTime > 0 && _moveInput.x > 0))) + IsSliding = true; + else + IsSliding = false; + #endregion + + #region GRAVITY + //Higher gravity if we've released the jump input or are falling + if (IsSliding) + { + SetGravityScale(0); + } + else if (RB.velocity.y < 0 && _moveInput.y < 0) + { + //Much higher gravity if holding down + SetGravityScale(Data.gravityScale * Data.fastFallGravityMult); + //Caps maximum fall speed, so when falling over large distances we don't accelerate to insanely high speeds + RB.velocity = new Vector2(RB.velocity.x, Mathf.Max(RB.velocity.y, -Data.maxFastFallSpeed)); + } + else if (_isJumpCut) + { + //Higher gravity if jump button released + SetGravityScale(Data.gravityScale * Data.jumpCutGravityMult); + RB.velocity = new Vector2(RB.velocity.x, Mathf.Max(RB.velocity.y, -Data.maxFallSpeed)); + } + else if ((IsJumping || IsWallJumping || _isJumpFalling) && Mathf.Abs(RB.velocity.y) < Data.jumpHangTimeThreshold) + { + SetGravityScale(Data.gravityScale * Data.jumpHangGravityMult); + } + else if (RB.velocity.y < 0) + { + //Higher gravity if falling + SetGravityScale(Data.gravityScale * Data.fallGravityMult); + //Caps maximum fall speed, so when falling over large distances we don't accelerate to insanely high speeds + RB.velocity = new Vector2(RB.velocity.x, Mathf.Max(RB.velocity.y, -Data.maxFallSpeed)); + } + else + { + //Default gravity if standing on a platform or moving upwards + SetGravityScale(Data.gravityScale); + } + #endregion } - void FixedUpdate() + private void FixedUpdate() { - Run(1); + //Handle Run + if (IsWallJumping) + Run(Data.wallJumpRunLerp); + else + Run(1); + + //Handle Slide + if (IsSliding) + Slide(); } - float AccelerationRate() + #region INPUT CALLBACKS + //Methods which whandle input detected in Update() + public void OnJumpInput() { - return this.runAcceleration / this.maxRunSpeed; + LastPressedJumpTime = Data.jumpInputBufferTime; } + public void OnJumpUpInput() + { + if (CanJumpCut() || CanWallJumpCut()) + _isJumpCut = true; + } + #endregion + + #region GENERAL METHODS + public void SetGravityScale(float scale) + { + RB.gravityScale = scale; + } + #endregion + + //MOVEMENT METHODS + #region RUN METHODS private void Run(float lerpAmount) { - float targetSpeed = this.movement.x * this.maxRunSpeed; + //Calculate the direction we want to move in and our desired velocity + float targetSpeed = _moveInput.x * Data.runMaxSpeed; + //We can reduce are control using Lerp() this smooths changes to are direction and speed + targetSpeed = Mathf.Lerp(RB.velocity.x, targetSpeed, lerpAmount); - float speedDiff = targetSpeed - this.rb.velocity.x; - forward = Mathf.Sign(speedDiff); + #region Calculate AccelRate + float accelRate; - float accel = AccelerationRate() * snappiness; + //Gets an acceleration value based on if we are accelerating (includes turning) + //or trying to decelerate (stop). As well as applying a multiplier if we're air borne. + if (LastOnGroundTime > 0) + accelRate = (Mathf.Abs(targetSpeed) > 0.01f) ? Data.runAccelAmount : Data.runDeccelAmount; + else + accelRate = (Mathf.Abs(targetSpeed) > 0.01f) ? Data.runAccelAmount * Data.accelInAir : Data.runDeccelAmount * Data.deccelInAir; + #endregion - float accelRate = (Mathf.Abs(targetSpeed) > 0.1) ? accel : -accel; - - float velPower = 1.0f; - - float move = Mathf.Pow(Mathf.Abs(speedDiff) * accelRate, velPower) * forward; - - this.onGround = IsGrounded(); - - float frictionAmount = 0.5f; - - // accelerate - if (onGround && (Mathf.Abs(this.movement.x) > 0.1f)) { // regular acceleration - this.rb.AddForce(move * Vector2.right, ForceMode2D.Force); - } else if (!onGround && (Mathf.Abs(this.movement.x) > 0.1f) && !playerBehavior.grapplingRope.isGrappling) { // while in air - this.rb.AddForce(move * Vector2.right * airSpeedMultiplier, ForceMode2D.Force); - } else if (!playerBehavior.grapplingRope.isGrappling) { // while grappling - this.rb.AddForce(move * Vector2.right * airSpeedMultiplier * airSpeedMultiplier, ForceMode2D.Force); - } - - // decelerate until stopped - if (onGround && Mathf.Abs(this.movement.x) < 0.1f) + #region Add Bonus Jump Apex Acceleration + //Increase are acceleration and maxSpeed when at the apex of their jump, makes the jump feel a bit more bouncy, responsive and natural + if ((IsJumping || IsWallJumping || _isJumpFalling) && Mathf.Abs(RB.velocity.y) < Data.jumpHangTimeThreshold) { - if (Mathf.Abs(rb.velocity.x) > 0.1f) { - float amount = Mathf.Min( - Mathf.Abs(this.rb.velocity.x), - Mathf.Abs(frictionAmount) - ); - amount *= Mathf.Sign(this.rb.velocity.x); - this.rb.AddForce(-amount * Vector2.right * snappiness, ForceMode2D.Impulse); - } else { - this.rb.velocity = new Vector2(0, rb.velocity.y); - } + accelRate *= Data.jumpHangAccelerationMult; + targetSpeed *= Data.jumpHangMaxSpeedMult; } + #endregion + + #region Conserve Momentum + //We won't slow the player down if they are moving in their desired direction but at a greater speed than their maxSpeed + if (Data.doConserveMomentum && Mathf.Abs(RB.velocity.x) > Mathf.Abs(targetSpeed) && Mathf.Sign(RB.velocity.x) == Mathf.Sign(targetSpeed) && Mathf.Abs(targetSpeed) > 0.01f && LastOnGroundTime < 0) + { + //Prevent any deceleration from happening, or in other words conserve are current momentum + //You could experiment with allowing for the player to slightly increae their speed whilst in this "state" + accelRate = 0; + } + #endregion + + //Calculate difference between current velocity and desired velocity + float speedDif = targetSpeed - RB.velocity.x; + //Calculate force along x-axis to apply to thr player + + float movement = speedDif * accelRate; + + //Convert this to a vector and apply to rigidbody + RB.AddForce(movement * Vector2.right, ForceMode2D.Force); + + /* + * For those interested here is what AddForce() will do + * RB.velocity = new Vector2(RB.velocity.x + (Time.fixedDeltaTime * speedDif * accelRate) / RB.mass, RB.velocity.y); + * Time.fixedDeltaTime is by default in Unity 0.02 seconds equal to 50 FixedUpdate() calls per second + */ } - bool IsGrounded() + private void Turn() { - if (Physics2D.BoxCast(transform.position, boxSize, 0, -transform.up, maxDistanceFromGround, groundLayer)) - { + //stores scale and flips the player along the x axis, + Vector3 scale = transform.localScale; + scale.x *= -1; + transform.localScale = scale; + + IsFacingRight = !IsFacingRight; + } + #endregion + + #region JUMP METHODS + private void Jump() + { + //Ensures we can't call Jump multiple times from one press + LastPressedJumpTime = 0; + LastOnGroundTime = 0; + + #region Perform Jump + //We increase the force applied if we are falling + //This means we'll always feel like we jump the same amount + //(setting the player's Y velocity to 0 beforehand will likely work the same, but I find this more elegant :D) + float force = Data.jumpForce; + if (RB.velocity.y < 0) + force -= RB.velocity.y; + + RB.AddForce(Vector2.up * force, ForceMode2D.Impulse); + #endregion + } + + private void WallJump(int dir) + { + //Ensures we can't call Wall Jump multiple times from one press + LastPressedJumpTime = 0; + LastOnGroundTime = 0; + LastOnWallRightTime = 0; + LastOnWallLeftTime = 0; + + #region Perform Wall Jump + Vector2 force = new Vector2(Data.wallJumpForce.x, Data.wallJumpForce.y); + force.x *= dir; //apply force in opposite direction of wall + + if (Mathf.Sign(RB.velocity.x) != Mathf.Sign(force.x)) + force.x -= RB.velocity.x; + + if (RB.velocity.y < 0) //checks whether player is falling, if so we subtract the velocity.y (counteracting force of gravity). This ensures the player always reaches our desired jump force or greater + force.y -= RB.velocity.y; + + //Unlike in the run we want to use the Impulse mode. + //The default mode will apply are force instantly ignoring masss + //RB.AddForce(force, ForceMode2D.Impulse); + #endregion + } + #endregion + + #region OTHER MOVEMENT METHODS + private void Slide() + { + //Works the same as the Run but only in the y-axis + //THis seems to work fine, buit maybe you'll find a better way to implement a slide into this system + float speedDif = Data.slideSpeed - RB.velocity.y; + float movement = speedDif * Data.slideAccel; + //So, we clamp the movement here to prevent any over corrections (these aren't noticeable in the Run) + //The force applied can't be greater than the (negative) speedDifference * by how many times a second FixedUpdate() is called. For more info research how force are applied to rigidbodies. + movement = Mathf.Clamp(movement, -Mathf.Abs(speedDif) * (1 / Time.fixedDeltaTime), Mathf.Abs(speedDif) * (1 / Time.fixedDeltaTime)); + + RB.AddForce(movement * Vector2.up); + } + #endregion + + + #region CHECK METHODS + public void CheckDirectionToFace(bool isMovingRight) + { + if (isMovingRight != IsFacingRight) + Turn(); + } + + private bool CanJump() + { + return LastOnGroundTime > 0 && !IsJumping; + } + + private bool CanWallJump() + { + return LastPressedJumpTime > 0 && LastOnWallTime > 0 && LastOnGroundTime <= 0 && (!IsWallJumping || + (LastOnWallRightTime > 0 && _lastWallJumpDir == 1) || (LastOnWallLeftTime > 0 && _lastWallJumpDir == -1)); + } + + private bool CanJumpCut() + { + return IsJumping && RB.velocity.y > 0; + } + + private bool CanWallJumpCut() + { + return IsWallJumping && RB.velocity.y > 0; + } + + public bool CanSlide() + { + if (LastOnWallTime > 0 && !IsJumping && !IsWallJumping && LastOnGroundTime <= 0) return true; - } - return false; + else + return false; } + #endregion - void OnDrawGizmos() + + #region EDITOR METHODS + private void OnDrawGizmosSelected() { - Gizmos.color = Color.red; - Gizmos.DrawCube(transform.position - transform.up * maxDistanceFromGround, boxSize); + Gizmos.color = Color.green; + Gizmos.DrawWireCube(_groundCheckPoint.position, _groundCheckSize); + Gizmos.color = Color.blue; + Gizmos.DrawWireCube(_frontWallCheckPoint.position, _wallCheckSize); + Gizmos.DrawWireCube(_backWallCheckPoint.position, _wallCheckSize); } + #endregion } diff --git a/Assets/Scripts/PlayerData.cs b/Assets/Scripts/PlayerData.cs index 6e777c9..0fc540b 100644 --- a/Assets/Scripts/PlayerData.cs +++ b/Assets/Scripts/PlayerData.cs @@ -4,11 +4,9 @@ using UnityEngine; public class PlayerData : ScriptableObject { [Header("Gravity")] - [HideInInspector] - public float gravityStrength; //Downwards force (gravity) needed for the desired jumpHeight and jumpTimeToApex. - [HideInInspector] - public float gravityScale; //Strength of the player's gravity as a multiplier of gravity (set in ProjectSettings/Physics2D). - //Also the value the player's rigidbody2D.gravityScale is set to. + [HideInInspector] public float gravityStrength; //Downwards force (gravity) needed for the desired jumpHeight and jumpTimeToApex. + [HideInInspector] public float gravityScale; //Strength of the player's gravity as a multiplier of gravity (set in ProjectSettings/Physics2D). + //Also the value the player's rigidbody2D.gravityScale is set to. [Space(5)] public float fallGravityMult; //Multiplier to the player's gravityScale when falling. public float maxFallSpeed; //Maximum fall speed (terminal velocity) of the player when falling. @@ -22,16 +20,12 @@ public class PlayerData : ScriptableObject [Header("Run")] public float runMaxSpeed; //Target speed we want the player to reach. public float runAcceleration; //The speed at which our player accelerates to max speed, can be set to runMaxSpeed for instant acceleration down to 0 for none at all - [HideInInspector] - public float runAccelAmount; //The actual force (multiplied with speedDiff) applied to the player. + [HideInInspector] public float runAccelAmount; //The actual force (multiplied with speedDiff) applied to the player. public float runDecceleration; //The speed at which our player decelerates from their current speed, can be set to runMaxSpeed for instant deceleration down to 0 for none at all - [HideInInspector] - public float runDeccelAmount; //Actual force (multiplied with speedDiff) applied to the player . + [HideInInspector] public float runDeccelAmount; //Actual force (multiplied with speedDiff) applied to the player . [Space(5)] - [Range(0f, 1)] - public float accelInAir; //Multipliers applied to acceleration rate when airborne. - [Range(0f, 1)] - public float deccelInAir; + [Range(0f, 1)] public float accelInAir; //Multipliers applied to acceleration rate when airborne. + [Range(0f, 1)] public float deccelInAir; [Space(5)] public bool doConserveMomentum = true; @@ -40,13 +34,11 @@ public class PlayerData : ScriptableObject [Header("Jump")] public float jumpHeight; //Height of the player's jump public float jumpTimeToApex; //Time between applying the jump force and reaching the desired jump height. These values also control the player's gravity and jump force. - [HideInInspector] - public float jumpForce; //The actual force applied (upwards) to the player when they jump. + [HideInInspector] public float jumpForce; //The actual force applied (upwards) to the player when they jump. [Header("Both Jumps")] public float jumpCutGravityMult; //Multiplier to increase gravity if the player releases thje jump button while still jumping - [Range(0f, 1)] - public float jumpHangGravityMult; //Reduces gravity while close to the apex (desired max height) of the jump + [Range(0f, 1)] public float jumpHangGravityMult; //Reduces gravity while close to the apex (desired max height) of the jump public float jumpHangTimeThreshold; //Speeds (close to 0) where the player will experience extra "jump hang". The player's velocity.y is closest to 0 at the jump's apex (think of the gradient of a parabola or quadratic function) [Space(0.5f)] public float jumpHangAccelerationMult; @@ -55,10 +47,8 @@ public class PlayerData : ScriptableObject [Header("Wall Jump")] public Vector2 wallJumpForce; //The actual force (this time set by us) applied to the player when wall jumping. [Space(5)] - [Range(0f, 1f)] - public float wallJumpRunLerp; //Reduces the effect of player's movement while wall jumping. - [Range(0f, 1.5f)] - public float wallJumpTime; //Time after wall jumping the player's movement is slowed for. + [Range(0f, 1f)] public float wallJumpRunLerp; //Reduces the effect of player's movement while wall jumping. + [Range(0f, 1.5f)] public float wallJumpTime; //Time after wall jumping the player's movement is slowed for. public bool doTurnOnWallJump; //Player will rotate to face wall jumping direction [Space(20)] @@ -68,10 +58,8 @@ public class PlayerData : ScriptableObject public float slideAccel; [Header("Assists")] - [Range(0.01f, 0.5f)] - public float coyoteTime; //Grace period after falling off a platform, where you can still jump - [Range(0.01f, 0.5f)] - public float jumpInputBufferTime; //Grace period after pressing jump where a jump will be automatically performed once the requirements (eg. being grounded) are met. + [Range(0.01f, 0.5f)] public float coyoteTime; //Grace period after falling off a platform, where you can still jump + [Range(0.01f, 0.5f)] public float jumpInputBufferTime; //Grace period after pressing jump where a jump will be automatically performed once the requirements (eg. being grounded) are met. //Unity Callback, called when the inspector updates diff --git a/Assets/Scripts/PlayerMovementData.asset b/Assets/Scripts/PlayerMovementData.asset new file mode 100644 index 0000000..9893b59 --- /dev/null +++ b/Assets/Scripts/PlayerMovementData.asset @@ -0,0 +1,44 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5ddb4766145463c75a48eeb29e77e0d3, type: 3} + m_Name: PlayerMovementData + m_EditorClassIdentifier: + gravityStrength: -20.408163 + gravityScale: 2.0803428 + fallGravityMult: 1.5 + maxFallSpeed: 25 + fastFallGravityMult: 2 + maxFastFallSpeed: 30 + runMaxSpeed: 5 + runAcceleration: 2.5 + runAccelAmount: 25 + runDecceleration: 5 + runDeccelAmount: 50 + accelInAir: 0.65 + deccelInAir: 0.65 + doConserveMomentum: 1 + jumpHeight: 5 + jumpTimeToApex: 0.7 + jumpForce: 14.285714 + jumpCutGravityMult: 2 + jumpHangGravityMult: 0.5 + jumpHangTimeThreshold: 1 + jumpHangAccelerationMult: 1.1 + jumpHangMaxSpeedMult: 1.3 + wallJumpForce: {x: 15, y: 25} + wallJumpRunLerp: 0.5 + wallJumpTime: 0.15 + doTurnOnWallJump: 0 + slideSpeed: 0 + slideAccel: 0 + coyoteTime: 0.1 + jumpInputBufferTime: 0.1 diff --git a/Assets/Scripts/PlayerMovementData.asset.meta b/Assets/Scripts/PlayerMovementData.asset.meta new file mode 100644 index 0000000..4830880 --- /dev/null +++ b/Assets/Scripts/PlayerMovementData.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: fec218a9d55267dedac6ebe31eab6dcd +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: