Unity3D之第一人称第三人称角色控制组件修改C#版本

转自: 雨松MOMO 2012年07月01日 于 雨松MOMO程序研究院 发表

之前MOMO在 Unity3D研究院之角色控制器组件研究(二十二)文章中向大家介绍了角色控制器组件。默认系统提供了JavaScript脚本的支持,可是我们还是喜欢用C#来编写脚本,本篇文章MOMO将把角色控制器的所有脚本全部改成C#语言。方便自己也方便大家学习,哇咔咔。首先,我们将角色控制器包引入工程中。如下图所示,默认提供的脚本除了MouseLook以外其它的都是JS脚本,本篇文章MOMO将把它们全部修改成C#。刚好也是答应Unity圣典的站长录制游戏开发视频,视频中我说下一节我将教大家怎么把角色控制器组件的脚本全部改成C#。

Unity3D研究院之第一人称第三人称角色控制组件修改C#版本(二十九) - 雨松MOMO程序研究院 - 1

首先把CharacterMotor.js修改成C# 它主要设置角色控制的系数,如运动、跳跃、移动、滑动等。第一人称与第三人称主角模型的移动与旋转的角度都最后都是在这里计算的,请大家好好看看这个类, 尤其是UpdateFunction()方法。

CharacterMotor.cs  

 

using UnityEngine;
using System.Collections;
 
/**
 *  @Author : www.xuanyusong.com 
 */
 
[RequireComponent(typeof(CharacterController))]
[AddComponentMenu("Character/Character Motor")]
 
public class CharacterMotor : MonoBehaviour {
 
// Does this script currently respond to input?
public bool canControl  = true;
 
public bool useFixedUpdate = true;
 
// For the next variables, @System.NonSerialized tells Unity to not serialize the variable or show it in the inspector view.
// Very handy for organization!
 
// The current global direction we want the character to move in.
[System.NonSerialized]
public Vector3 inputMoveDirection = Vector3.zero;
 
// Is the jump button held down? We use this interface instead of checking
// for the jump button directly so this script can also be used by AIs.
[System.NonSerialized]
public bool inputJump  = false;
 
[System.Serializable]
public class CharacterMotorMovement
{
 
	// The maximum horizontal speed when moving
	public float maxForwardSpeed = 10.0f;
	public float maxSidewaysSpeed = 10.0f;
	public float maxBackwardsSpeed = 10.0f;
 
	// Curve for multiplying speed based on slope (negative = downwards)
	public AnimationCurve slopeSpeedMultiplier = new AnimationCurve(new Keyframe(-90, 1), new Keyframe(0, 1), new Keyframe(90, 0));
 
	// How fast does the character change speeds?  Higher is faster.
	public float maxGroundAcceleration = 30.0f;
	public float maxAirAcceleration = 20.0f;
 
	// The gravity for the character
	public float gravity = 10.0f;
	public float maxFallSpeed = 20.0f;
 
	// For the next variables, @System.NonSerialized tells Unity to not serialize the variable or show it in the inspector view.
	// Very handy for organization!
 
	// The last collision flags returned from controller.Move
	[System.NonSerialized]
	public CollisionFlags collisionFlags; 
 
	// We will keep track of the character's current velocity,
	[System.NonSerialized]
	public Vector3 velocity;
 
	// This keeps track of our current velocity while we're not grounded
	[System.NonSerialized]
	public Vector3 frameVelocity = Vector3.zero;
 
	[System.NonSerialized]
	public Vector3 hitPoint = Vector3.zero;
 
	[System.NonSerialized]
	public Vector3 lastHitPoint = new Vector3(Mathf.Infinity, 0, 0);
}
 
public CharacterMotorMovement movement = new CharacterMotorMovement();
 
public enum MovementTransferOnJump {
	None, // The jump is not affected by velocity of floor at all.
	InitTransfer, // Jump gets its initial velocity from the floor, then gradualy comes to a stop.
	PermaTransfer, // Jump gets its initial velocity from the floor, and keeps that velocity until landing.
	PermaLocked // Jump is relative to the movement of the last touched floor and will move together with that floor.
}
 
// We will contain all the jumping related variables in one helper class for clarity.	
[System.Serializable]
public class CharacterMotorJumping {
	// Can the character jump?
	public bool enabled = true;
 
	// How high do we jump when pressing jump and letting go immediately
	public float baseHeight = 1.0f;
 
	// We add extraHeight units (meters) on top when holding the button down longer while jumping
	public float extraHeight = 4.1f;
 
	// How much does the character jump out perpendicular to the surface on walkable surfaces?
	// 0 means a fully vertical jump and 1 means fully perpendicular.
	public float perpAmount  = 0.0f;
 
	// How much does the character jump out perpendicular to the surface on too steep surfaces?
	// 0 means a fully vertical jump and 1 means fully perpendicular.
	public float steepPerpAmount = 0.5f;
 
	// For the next variables, @System.NonSerialized tells Unity to not serialize the variable or show it in the inspector view.
	// Very handy for organization!
 
	// Are we jumping? (Initiated with jump button and not grounded yet)
	// To see if we are just in the air (initiated by jumping OR falling) see the grounded variable.
	[System.NonSerialized]
	public bool jumping = false;
 
	[System.NonSerialized]
	public bool holdingJumpButton = false;
 
	// the time we jumped at (Used to determine for how long to apply extra jump power after jumping.)
	[System.NonSerialized]
	public float lastStartTime = 0.0f;
 
	[System.NonSerialized]
	public float lastButtonDownTime = -100f;
 
	[System.NonSerialized]
	public Vector3 jumpDir = Vector3.up;
}
 
public CharacterMotorJumping  jumping = new CharacterMotorJumping();
 
[System.Serializable]
public class CharacterMotorMovingPlatform {
	public bool enabled = true;
 
	public MovementTransferOnJump movementTransfer = MovementTransferOnJump.PermaTransfer;
 
	[System.NonSerialized]
	public Transform hitPlatform;
 
	[System.NonSerialized]
	public Transform activePlatform;
 
	[System.NonSerialized]
	public Vector3 activeLocalPoint;
 
	[System.NonSerialized]
	public Vector3 activeGlobalPoint;
 
	[System.NonSerialized]
	public Quaternion activeLocalRotation;
 
	[System.NonSerialized]
	public Quaternion activeGlobalRotation;
 
	[System.NonSerialized]
	public Matrix4x4 lastMatrix;
 
	[System.NonSerialized]
	public Vector3 platformVelocity;
 
	[System.NonSerialized]
	public bool newPlatform;
}
 
public CharacterMotorMovingPlatform movingPlatform  = new CharacterMotorMovingPlatform();
 
[System.Serializable]
public class CharacterMotorSliding {
	// Does the character slide on too steep surfaces?
	public bool enabled = true;
 
	// How fast does the character slide on steep surfaces?
	public float slidingSpeed  = 15f;
 
	// How much can the player control the sliding direction?
	// If the value is 0.5 the player can slide sideways with half the speed of the downwards sliding speed.
	public float sidewaysControl = 1.0f;
 
	// How much can the player influence the sliding speed?
	// If the value is 0.5 the player can speed the sliding up to 150% or slow it down to 50%.
	public float speedControl  = 0.4f;
}
 
public CharacterMotorSliding sliding  = new CharacterMotorSliding();
 
[System.NonSerialized]
public bool grounded = true;
 
[System.NonSerialized]
public Vector3 groundNormal = Vector3.zero;
 
private Vector3  lastGroundNormal = Vector3.zero;
 
private Transform tr;
 
private CharacterController  controller ;
 
void Awake () {
	controller = GetComponent <CharacterController>();
	tr = transform;
}
 
private void UpdateFunction () {
	// We copy the actual velocity into a temporary variable that we can manipulate.
	Vector3 velocity  = movement.velocity;
 
	// Update velocity based on input
	velocity = ApplyInputVelocityChange(velocity);
 
	// Apply gravity and jumping force
	velocity = ApplyGravityAndJumping (velocity);
 
	// Moving platform support
	Vector3 moveDistance  = Vector3.zero;
	if (MoveWithPlatform()) {
		Vector3 newGlobalPoint  = movingPlatform.activePlatform.TransformPoint(movingPlatform.activeLocalPoint);
		moveDistance = (newGlobalPoint - movingPlatform.activeGlobalPoint);
		if (moveDistance != Vector3.zero)
			controller.Move(moveDistance);
 
		// Support moving platform rotation as well:
        Quaternion newGlobalRotation  = movingPlatform.activePlatform.rotation * movingPlatform.activeLocalRotation;
        Quaternion rotationDiff  = newGlobalRotation * Quaternion.Inverse(movingPlatform.activeGlobalRotation);
 
        var yRotation = rotationDiff.eulerAngles.y;
        if (yRotation != 0) {
	        // Prevent rotation of the local up vector
	        tr.Rotate(0, yRotation, 0);
        }
	}
 
	// Save lastPosition for velocity calculation.
	Vector3 lastPosition  = tr.position;
 
	// We always want the movement to be framerate independent.  Multiplying by Time.deltaTime does this.
	Vector3 currentMovementOffset = velocity * Time.deltaTime;
 
	// Find out how much we need to push towards the ground to avoid loosing grouning
	// when walking down a step or over a sharp change in slope.
	float pushDownOffset  = Mathf.Max(controller.stepOffset, new Vector3(currentMovementOffset.x, 0, currentMovementOffset.z).magnitude);
	if (grounded)
		currentMovementOffset -= pushDownOffset * Vector3.up;
 
	// Reset variables that will be set by collision function
	movingPlatform.hitPlatform = null;
	groundNormal = Vector3.zero;
 
   	// Move our character!
	movement.collisionFlags = controller.Move (currentMovementOffset);
 
	movement.lastHitPoint = movement.hitPoint;
	lastGroundNormal = groundNormal;
 
	if (movingPlatform.enabled && movingPlatform.activePlatform != movingPlatform.hitPlatform) {
		if (movingPlatform.hitPlatform != null) {
			movingPlatform.activePlatform = movingPlatform.hitPlatform;
			movingPlatform.lastMatrix = movingPlatform.hitPlatform.localToWorldMatrix;
			movingPlatform.newPlatform = true;
		}
	}
 
	// Calculate the velocity based on the current and previous position.  
	// This means our velocity will only be the amount the character actually moved as a result of collisions.
	Vector3 oldHVelocity  = new Vector3(velocity.x, 0, velocity.z);
	movement.velocity = (tr.position - lastPosition) / Time.deltaTime;
	Vector3 newHVelocity  = new Vector3(movement.velocity.x, 0, movement.velocity.z);
 
	// The CharacterController can be moved in unwanted directions when colliding with things.
	// We want to prevent this from influencing the recorded velocity.
	if (oldHVelocity == Vector3.zero) {
		movement.velocity = new Vector3(0, movement.velocity.y, 0);
	}
	else {
		float projectedNewVelocity  = Vector3.Dot(newHVelocity, oldHVelocity) / oldHVelocity.sqrMagnitude;
		movement.velocity = oldHVelocity * Mathf.Clamp01(projectedNewVelocity) + movement.velocity.y * Vector3.up;
	}
 
	if (movement.velocity.y < velocity.y - 0.001) {
		if (movement.velocity.y < 0) {
			// Something is forcing the CharacterController down faster than it should.
			// Ignore this
			movement.velocity.y = velocity.y;
		}
		else {
			// The upwards movement of the CharacterController has been blocked.
			// This is treated like a ceiling collision - stop further jumping here.
			jumping.holdingJumpButton = false;
		}
	}
 
	// We were grounded but just loosed grounding
	if (grounded && !IsGroundedTest()) {
		grounded = false;
 
		// Apply inertia from platform
		if (movingPlatform.enabled &&
			(movingPlatform.movementTransfer == MovementTransferOnJump.InitTransfer ||
			movingPlatform.movementTransfer == MovementTransferOnJump.PermaTransfer)
		) {
			movement.frameVelocity = movingPlatform.platformVelocity;
			movement.velocity += movingPlatform.platformVelocity;
		}
 
		SendMessage("OnFall", SendMessageOptions.DontRequireReceiver);
		// We pushed the character down to ensure it would stay on the ground if there was any.
		// But there wasn't so now we cancel the downwards offset to make the fall smoother.
		tr.position += pushDownOffset * Vector3.up;
	}
	// We were not grounded but just landed on something
	else if (!grounded && IsGroundedTest()) {
		grounded = true;
		jumping.jumping = false;
		SubtractNewPlatformVelocity();
 
		SendMessage("OnLand", SendMessageOptions.DontRequireReceiver);
	}
 
	// Moving platforms support
	if (MoveWithPlatform()) {
		// Use the center of the lower half sphere of the capsule as reference point.
		// This works best when the character is standing on moving tilting platforms. 
		movingPlatform.activeGlobalPoint = tr.position + Vector3.up * (controller.center.y - controller.height*0.5f + controller.radius);
		movingPlatform.activeLocalPoint = movingPlatform.activePlatform.InverseTransformPoint(movingPlatform.activeGlobalPoint);
 
		// Support moving platform rotation as well:
        movingPlatform.activeGlobalRotation = tr.rotation;
        movingPlatform.activeLocalRotation = Quaternion.Inverse(movingPlatform.activePlatform.rotation) * movingPlatform.activeGlobalRotation; 
	}
}
 
void FixedUpdate () {
	if (movingPlatform.enabled) {
		if (movingPlatform.activePlatform != null) {
			if (!movingPlatform.newPlatform) {
				Vector3 lastVelocity  = movingPlatform.platformVelocity;
 
				movingPlatform.platformVelocity = (
					movingPlatform.activePlatform.localToWorldMatrix.MultiplyPoint3x4(movingPlatform.activeLocalPoint)
					- movingPlatform.lastMatrix.MultiplyPoint3x4(movingPlatform.activeLocalPoint)
				) / Time.deltaTime;
			}
			movingPlatform.lastMatrix = movingPlatform.activePlatform.localToWorldMatrix;
			movingPlatform.newPlatform = false;
		}
		else {
			movingPlatform.platformVelocity = Vector3.zero;	
		}
	}
 
	if (useFixedUpdate)
		UpdateFunction();
}
 
void Update () {
	if (!useFixedUpdate)
		UpdateFunction();
}
 
private Vector3 ApplyInputVelocityChange (Vector3 velocity) {	
	if (!canControl)
		inputMoveDirection = Vector3.zero;
 
	// Find desired velocity
	Vector3 desiredVelocity;
	if (grounded && TooSteep()) {
		// The direction we're sliding in
		desiredVelocity = new Vector3(groundNormal.x, 0, groundNormal.z).normalized;
		// Find the input movement direction projected onto the sliding direction
		var projectedMoveDir = Vector3.Project(inputMoveDirection, desiredVelocity);
		// Add the sliding direction, the spped control, and the sideways control vectors
		desiredVelocity = desiredVelocity + projectedMoveDir * sliding.speedControl + (inputMoveDirection - projectedMoveDir) * sliding.sidewaysControl;
		// Multiply with the sliding speed
		desiredVelocity *= sliding.slidingSpeed;
	}
	else
		desiredVelocity = GetDesiredHorizontalVelocity();
 
	if (movingPlatform.enabled && movingPlatform.movementTransfer == MovementTransferOnJump.PermaTransfer) {
		desiredVelocity += movement.frameVelocity;
		desiredVelocity.y = 0;
	}
 
	if (grounded)
		desiredVelocity = AdjustGroundVelocityToNormal(desiredVelocity, groundNormal);
	else
		velocity.y = 0;
 
	// Enforce max velocity change
	float maxVelocityChange  = GetMaxAcceleration(grounded) * Time.deltaTime;
	Vector3 velocityChangeVector  = (desiredVelocity - velocity);
	if (velocityChangeVector.sqrMagnitude > maxVelocityChange * maxVelocityChange) {
		velocityChangeVector = velocityChangeVector.normalized * maxVelocityChange;
	}
	// If we're in the air and don't have control, don't apply any velocity change at all.
	// If we're on the ground and don't have control we do apply it - it will correspond to friction.
	if (grounded || canControl)
		velocity += velocityChangeVector;
 
	if (grounded) {
		// When going uphill, the CharacterController will automatically move up by the needed amount.
		// Not moving it upwards manually prevent risk of lifting off from the ground.
		// When going downhill, DO move down manually, as gravity is not enough on steep hills.
		velocity.y = Mathf.Min(velocity.y, 0);
	}
 
	return velocity;
}
 
private Vector3 ApplyGravityAndJumping (Vector3 velocity) {
 
	if (!inputJump || !canControl) {
		jumping.holdingJumpButton = false;
		jumping.lastButtonDownTime = -100;
	}
 
	if (inputJump && jumping.lastButtonDownTime < 0 && canControl)
		jumping.lastButtonDownTime = Time.time;
 
	if (grounded)
		velocity.y = Mathf.Min(0, velocity.y) - movement.gravity * Time.deltaTime;
	else {
		velocity.y = movement.velocity.y - movement.gravity * Time.deltaTime;
 
		// When jumping up we don't apply gravity for some time when the user is holding the jump button.
		// This gives more control over jump height by pressing the button longer.
		if (jumping.jumping && jumping.holdingJumpButton) {
			// Calculate the duration that the extra jump force should have effect.
			// If we're still less than that duration after the jumping time, apply the force.
			if (Time.time < jumping.lastStartTime + jumping.extraHeight / CalculateJumpVerticalSpeed(jumping.baseHeight)) {
				// Negate the gravity we just applied, except we push in jumpDir rather than jump upwards.
				velocity += jumping.jumpDir * movement.gravity * Time.deltaTime;
			}
		}
 
		// Make sure we don't fall any faster than maxFallSpeed. This gives our character a terminal velocity.
		velocity.y = Mathf.Max (velocity.y, -movement.maxFallSpeed);
	}
 
	if (grounded) {
		// Jump only if the jump button was pressed down in the last 0.2 seconds.
		// We use this check instead of checking if it's pressed down right now
		// because players will often try to jump in the exact moment when hitting the ground after a jump
		// and if they hit the button a fraction of a second too soon and no new jump happens as a consequence,
		// it's confusing and it feels like the game is buggy.
		if (jumping.enabled && canControl && (Time.time - jumping.lastButtonDownTime < 0.2)) {
			grounded = false;
			jumping.jumping = true;
			jumping.lastStartTime = Time.time;
			jumping.lastButtonDownTime = -100;
			jumping.holdingJumpButton = true;
 
			// Calculate the jumping direction
			if (TooSteep())
				jumping.jumpDir = Vector3.Slerp(Vector3.up, groundNormal, jumping.steepPerpAmount);
			else
				jumping.jumpDir = Vector3.Slerp(Vector3.up, groundNormal, jumping.perpAmount);
 
			// Apply the jumping force to the velocity. Cancel any vertical velocity first.
			velocity.y = 0;
			velocity += jumping.jumpDir * CalculateJumpVerticalSpeed (jumping.baseHeight);
 
			// Apply inertia from platform
			if (movingPlatform.enabled &&
				(movingPlatform.movementTransfer == MovementTransferOnJump.InitTransfer ||
				movingPlatform.movementTransfer == MovementTransferOnJump.PermaTransfer)
			) {
				movement.frameVelocity = movingPlatform.platformVelocity;
				velocity += movingPlatform.platformVelocity;
			}
 
			SendMessage("OnJump", SendMessageOptions.DontRequireReceiver);
		}
		else {
			jumping.holdingJumpButton = false;
		}
	}
 
	return velocity;
}
 
void OnControllerColliderHit (ControllerColliderHit hit) {
	if (hit.normal.y > 0 && hit.normal.y > groundNormal.y && hit.moveDirection.y < 0) {
		if ((hit.point - movement.lastHitPoint).sqrMagnitude > 0.001 || lastGroundNormal == Vector3.zero)
			groundNormal = hit.normal;
		else
			groundNormal = lastGroundNormal;
 
		movingPlatform.hitPlatform = hit.collider.transform;
		movement.hitPoint = hit.point;
		movement.frameVelocity = Vector3.zero;
	}
}
 
private IEnumerator SubtractNewPlatformVelocity () {
	// When landing, subtract the velocity of the new ground from the character's velocity
	// since movement in ground is relative to the movement of the ground.
	if (movingPlatform.enabled &&
		(movingPlatform.movementTransfer == MovementTransferOnJump.InitTransfer ||
		movingPlatform.movementTransfer == MovementTransferOnJump.PermaTransfer)
	) {
		// If we landed on a new platform, we have to wait for two FixedUpdates
		// before we know the velocity of the platform under the character
		if (movingPlatform.newPlatform) {
			Transform platform  = movingPlatform.activePlatform;
			yield return new WaitForFixedUpdate();
			yield return new WaitForFixedUpdate();
			if (grounded && platform == movingPlatform.activePlatform)
				yield return 1;
		}
		movement.velocity -= movingPlatform.platformVelocity;
	}
}
 
private bool MoveWithPlatform () {
	return (
		movingPlatform.enabled
		&& (grounded || movingPlatform.movementTransfer == MovementTransferOnJump.PermaLocked)
		&& movingPlatform.activePlatform != null
	);
}
 
private Vector3 GetDesiredHorizontalVelocity () {
	// Find desired velocity
	Vector3 desiredLocalDirection  = tr.InverseTransformDirection(inputMoveDirection);
	float maxSpeed  = MaxSpeedInDirection(desiredLocalDirection);
	if (grounded) {
		// Modify max speed on slopes based on slope speed multiplier curve
		var movementSlopeAngle = Mathf.Asin(movement.velocity.normalized.y)  * Mathf.Rad2Deg;
		maxSpeed *= movement.slopeSpeedMultiplier.Evaluate(movementSlopeAngle);
	}
	return tr.TransformDirection(desiredLocalDirection * maxSpeed);
}
 
private Vector3 AdjustGroundVelocityToNormal (Vector3 hVelocity, Vector3 groundNormal) {
	Vector3 sideways  = Vector3.Cross(Vector3.up, hVelocity);
	return Vector3.Cross(sideways, groundNormal).normalized * hVelocity.magnitude;
}
 
private bool IsGroundedTest () {
	return (groundNormal.y > 0.01);
}
 
float GetMaxAcceleration (bool grounded) {
	// Maximum acceleration on ground and in air
	if (grounded)
		return movement.maxGroundAcceleration;
	else
		return movement.maxAirAcceleration;
}
 
float CalculateJumpVerticalSpeed (float targetJumpHeight) {
	// From the jump height and gravity we deduce the upwards speed 
	// for the character to reach at the apex.
	return Mathf.Sqrt (2 * targetJumpHeight * movement.gravity);
}
 
bool IsJumping () {
	return jumping.jumping;
}
 
bool IsSliding () {
	return (grounded && sliding.enabled && TooSteep());
}
 
bool IsTouchingCeiling () {
	return (movement.collisionFlags & CollisionFlags.CollidedAbove) != 0;
}
 
bool IsGrounded () {
	return grounded;
}
 
bool TooSteep () {
	return (groundNormal.y <= Mathf.Cos(controller.slopeLimit * Mathf.Deg2Rad));
}
 
Vector3 GetDirection () {
	return inputMoveDirection;
}
 
void  SetControllable (bool controllable) {
	canControl = controllable;
}
 
// Project a direction onto elliptical quater segments based on forward, sideways, and backwards speed.
// The function returns the length of the resulting vector.
float MaxSpeedInDirection (Vector3 desiredMovementDirection) {
	if (desiredMovementDirection == Vector3.zero)
		return 0;
	else {
		float zAxisEllipseMultiplier = (desiredMovementDirection.z > 0 ? movement.maxForwardSpeed : movement.maxBackwardsSpeed) / movement.maxSidewaysSpeed;
		Vector3 temp = new Vector3(desiredMovementDirection.x, 0, desiredMovementDirection.z / zAxisEllipseMultiplier).normalized;
		float length = new Vector3(temp.x, 0, temp.z * zAxisEllipseMultiplier).magnitude * movement.maxSidewaysSpeed;
		return length;
	}
}
 
void SetVelocity (Vector3 velocity) {
	grounded = false;
	movement.velocity = velocity;
	movement.frameVelocity = Vector3.zero;
	SendMessage("OnExternalVelocity");
}
 
// Require a character controller to be attached to the same game object
 
//@script RequireComponent (CharacterController)
//@script AddComponentMenu ("Character/Character Motor")
 
}

接着把FPSInputControoler.js修改成C#语言。它用于第一人称控制角色移动,这里会监听主角按下的方向键最后传给CharacterMotor去计算模型的位置与旋转的角度。

FPSInputController.cs

using UnityEngine;
using System.Collections;
 
/**
 *  @Author : www.xuanyusong.com 
 */
 
[RequireComponent(typeof(CharacterMotor))]
[AddComponentMenu("Character/FPS Input Controller")]
 
public class FPSInputController : MonoBehaviour {
 
private CharacterMotor motor ;
 
// Use this for initialization
void Awake () {
	motor = GetComponent<CharacterMotor>();
}
 
// Update is called once per frame
void Update () {
	// Get the input vector from kayboard or analog stick
	Vector3 directionVector = new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical"));
 
	if (directionVector != Vector3.zero) {
		// Get the length of the directon vector and then normalize it
		// Dividing by the length is cheaper than normalizing when we already have the length anyway
		var directionLength = directionVector.magnitude;
		directionVector = directionVector / directionLength;
 
		// Make sure the length is no bigger than 1
		directionLength = Mathf.Min(1, directionLength);
 
		// Make the input vector more sensitive towards the extremes and less sensitive in the middle
		// This makes it easier to control slow speeds when using analog sticks
		directionLength = directionLength * directionLength;
 
		// Multiply the normalized direction vector by the modified length
		directionVector = directionVector * directionLength;
	}
 
	// Apply the direction to the CharacterMotor
	motor.inputMoveDirection = transform.rotation * directionVector;
	motor.inputJump = Input.GetButton("Jump");
}
}

MouseLook.cs因为已经是C#语言所以就不翻译了,然后是PlatFormInputController.cs 把它转成C#语言。它和FPSInputController一样会控制主角,但是它会更加精细的计算模型旋转的插值系数。

PlatformInputController.cs

using UnityEngine;
using System.Collections;
 
/**
 *  @Author : www.xuanyusong.com 
 */
 
[RequireComponent(typeof(CharacterController))]
[AddComponentMenu("Character/Platform Input Controller")]
public class PlatformInputController : MonoBehaviour {
 
public bool autoRotate = true;
public float  maxRotationSpeed = 360;
 
private CharacterMotor motor ;
 
// Use this for initialization
void Awake () {
	motor = GetComponent<CharacterMotor>();
}
 
// Update is called once per frame
void Update () {
	// Get the input vector from kayboard or analog stick
	Vector3 directionVector = new Vector3(Input.GetAxis("Horizontal"), Input.GetAxis("Vertical"), 0);
 
	if (directionVector != Vector3.zero) {
		// Get the length of the directon vector and then normalize it
		// Dividing by the length is cheaper than normalizing when we already have the length anyway
		var directionLength = directionVector.magnitude;
		directionVector = directionVector / directionLength;
 
		// Make sure the length is no bigger than 1
		directionLength = Mathf.Min(1, directionLength);
 
		// Make the input vector more sensitive towards the extremes and less sensitive in the middle
		// This makes it easier to control slow speeds when using analog sticks
		directionLength = directionLength * directionLength;
 
		// Multiply the normalized direction vector by the modified length
		directionVector = directionVector * directionLength;
	}
 
	// Rotate the input vector into camera space so up is camera's up and right is camera's right
	directionVector = Camera.main.transform.rotation * directionVector;
 
	// Rotate input vector to be perpendicular to character's up vector
	var camToCharacterSpace = Quaternion.FromToRotation(-Camera.main.transform.forward, transform.up);
	directionVector = (camToCharacterSpace * directionVector);
 
	// Apply the direction to the CharacterMotor
	motor.inputMoveDirection = directionVector;
	motor.inputJump = Input.GetButton("Jump");
 
	// Set rotation to the move direction	
	if (autoRotate && directionVector.sqrMagnitude > 0.01) {
		Vector3 newForward  = ConstantSlerp(
			transform.forward,
			directionVector,
			maxRotationSpeed * Time.deltaTime
		);
		newForward = ProjectOntoPlane(newForward, transform.up);
		transform.rotation = Quaternion.LookRotation(newForward, transform.up);
	}
}
 
Vector3 ProjectOntoPlane (Vector3 v, Vector3 normal) {
	return v - Vector3.Project(v, normal);
}
 
Vector3 ConstantSlerp (Vector3 from, Vector3 to, float angle) {
	float value = Mathf.Min(1, angle / Vector3.Angle(from, to));
	return Vector3.Slerp(from, to, value);
}
}

接着是ThirdPersonCamera 我们把它改成C#语言。它主要控制第三人称视角时摄像机的控制。

ThirdPersonCamera.cs

using UnityEngine;
using System.Collections;
 
/**
 *  @Author : www.xuanyusong.com 
 */
 
public class ThirdPersonCamera : MonoBehaviour {
 
public Transform cameraTransform;
private Transform _target;
 
public float distance = 7.0f;
 
public float height = 3.0f;
 
public float angularSmoothLag = 0.3f;
public float angularMaxSpeed = 15.0f;
 
public float heightSmoothLag = 0.3f;
 
public float snapSmoothLag = 0.2f;
public float snapMaxSpeed = 720.0f;
 
public float clampHeadPositionScreenSpace = 0.75f;
 
public float lockCameraTimeout = 0.2f;
 
private Vector3 headOffset = Vector3.zero;
private Vector3 centerOffset = Vector3.zero;
 
private float heightVelocity = 0.0f;
private float  angleVelocity = 0.0f;
private bool snap = false;
private ThirdPersonController controller;
private float targetHeight = 100000.0f; 
 
void Awake ()
{
	if(!cameraTransform && Camera.main)
		cameraTransform = Camera.main.transform;
	if(!cameraTransform) {
		Debug.Log("Please assign a camera to the ThirdPersonCamera script.");
		enabled = false;	
	}
 
	_target = transform;
	if (_target)
	{
		controller = _target.GetComponent<ThirdPersonController>();
	}
 
	if (controller)
	{
		CharacterController characterController  = (CharacterController)_target.collider;
		centerOffset = characterController.bounds.center - _target.position;
		headOffset = centerOffset;
		headOffset.y = characterController.bounds.max.y - _target.position.y;
	}
	else
		Debug.Log("Please assign a target to the camera that has a ThirdPersonController script attached.");
 
	Cut(_target, centerOffset);
}
 
void DebugDrawStuff ()
{
	Debug.DrawLine(_target.position, _target.position + headOffset);
 
}
 
float  AngleDistance (float a , float b )
{
	a = Mathf.Repeat(a, 360);
	b = Mathf.Repeat(b, 360);
 
	return Mathf.Abs(b - a);
}
 
void  Apply (Transform dummyTarget, Vector3 dummyCenter)
{
	// Early out if we don't have a target
	if (!controller)
		return;
 
	Vector3 targetCenter = _target.position + centerOffset;
	Vector3 targetHead = _target.position + headOffset;
 
//	DebugDrawStuff();
 
	// Calculate the current & target rotation angles
	float originalTargetAngle = _target.eulerAngles.y;
	float currentAngle = cameraTransform.eulerAngles.y;
 
	// Adjust real target angle when camera is locked
	float targetAngle = originalTargetAngle; 
 
	// When pressing Fire2 (alt) the camera will snap to the target direction real quick.
	// It will stop snapping when it reaches the target
	if (Input.GetButton("Fire2"))
		snap = true;
 
	if (snap)
	{
		// We are close to the target, so we can stop snapping now!
		if (AngleDistance (currentAngle, originalTargetAngle) < 3.0)
			snap = false;
 
		currentAngle = Mathf.SmoothDampAngle(currentAngle, targetAngle, ref angleVelocity, snapSmoothLag, snapMaxSpeed);
	}
	// Normal camera motion
	else
	{
 
		if (controller.GetLockCameraTimer () < lockCameraTimeout)
		{
			targetAngle = currentAngle;
		}
 
		// Lock the camera when moving backwards!
		// * It is really confusing to do 180 degree spins when turning around.
		if (AngleDistance (currentAngle, targetAngle) > 160 && controller.IsMovingBackwards ())
			targetAngle += 180;
 
		currentAngle = Mathf.SmoothDampAngle(currentAngle, targetAngle, ref angleVelocity, angularSmoothLag, angularMaxSpeed);
	}
 
	// When jumping don't move camera upwards but only down!
	if (controller.IsJumping ())
	{
		// We'd be moving the camera upwards, do that only if it's really high
		float newTargetHeight = targetCenter.y + height;
		if (newTargetHeight < targetHeight || newTargetHeight - targetHeight > 5)
			targetHeight = targetCenter.y + height;
	}
	// When walking always update the target height
	else
	{
		targetHeight = targetCenter.y + height;
	}
 
	// Damp the height
	float currentHeight = cameraTransform.position.y;
	currentHeight = Mathf.SmoothDamp (currentHeight, targetHeight, ref heightVelocity, heightSmoothLag);
 
	// Convert the angle into a rotation, by which we then reposition the camera
	Quaternion currentRotation = Quaternion.Euler (0, currentAngle, 0);
 
	// Set the position of the camera on the x-z plane to:
	// distance meters behind the target
	cameraTransform.position = targetCenter;
	cameraTransform.position += currentRotation * Vector3.back * distance;
 
	// Set the height of the camera
    cameraTransform.position = new Vector3(cameraTransform.position.x,currentHeight,cameraTransform.position.z);
 
	// Always look at the target	
	SetUpRotation(targetCenter, targetHead);
}
 
void LateUpdate () {
	Apply (transform, Vector3.zero);
}
 
void  Cut (Transform dummyTarget , Vector3 dummyCenter)
{
	float oldHeightSmooth = heightSmoothLag;
	float oldSnapMaxSpeed = snapMaxSpeed;
	float oldSnapSmooth = snapSmoothLag;
 
	snapMaxSpeed = 10000;
	snapSmoothLag = 0.001f;
	heightSmoothLag = 0.001f;
 
	snap = true;
	Apply (transform, Vector3.zero);
 
	heightSmoothLag = oldHeightSmooth;
	snapMaxSpeed = oldSnapMaxSpeed;
	snapSmoothLag = oldSnapSmooth;
}
 
void SetUpRotation (Vector3 centerPos,Vector3  headPos)
{
	// Now it's getting hairy. The devil is in the details here, the big issue is jumping of course.
	// * When jumping up and down we don't want to center the guy in screen space.
	//  This is important to give a feel for how high you jump and avoiding large camera movements.
	//   
	// * At the same time we dont want him to ever go out of screen and we want all rotations to be totally smooth.
	//
	// So here is what we will do:
	//
	// 1. We first find the rotation around the y axis. Thus he is always centered on the y-axis
	// 2. When grounded we make him be centered
	// 3. When jumping we keep the camera rotation but rotate the camera to get him back into view if his head is above some threshold
	// 4. When landing we smoothly interpolate towards centering him on screen
	Vector3 cameraPos = cameraTransform.position;
	Vector3 offsetToCenter = centerPos - cameraPos;
 
	// Generate base rotation only around y-axis
	Quaternion yRotation = Quaternion.LookRotation(new Vector3(offsetToCenter.x, 0, offsetToCenter.z));
 
	Vector3 relativeOffset = Vector3.forward * distance + Vector3.down * height;
	cameraTransform.rotation = yRotation * Quaternion.LookRotation(relativeOffset);
 
	// Calculate the projected center position and top position in world space
	Ray centerRay = cameraTransform.camera.ViewportPointToRay(new Vector3(0.5f, 0.5f, 1f));
	Ray topRay = cameraTransform.camera.ViewportPointToRay(new Vector3(0.5f, clampHeadPositionScreenSpace, 1f));
 
	Vector3 centerRayPos = centerRay.GetPoint(distance);
	Vector3 topRayPos = topRay.GetPoint(distance);
 
	float centerToTopAngle = Vector3.Angle(centerRay.direction, topRay.direction);
 
	float heightToAngle = centerToTopAngle / (centerRayPos.y - topRayPos.y);
 
	float extraLookAngle = heightToAngle * (centerRayPos.y - centerPos.y);
	if (extraLookAngle < centerToTopAngle)
	{
		extraLookAngle = 0;
	}
	else
	{
		extraLookAngle = extraLookAngle - centerToTopAngle;
		cameraTransform.rotation *= Quaternion.Euler(-extraLookAngle, 0, 0);
	}
}
 
	Vector3 GetCenterOffset ()
	{
		return centerOffset;
	}
 
}

最后一个是ThirdPersonController我们同样把它修改成C#语言,它主要更新第三人称视角控制主角时播放的各种动画,主角移动,等等。

ThirdPersonController.cs

using UnityEngine;
using System.Collections;
 
/**
 *  @Author : www.xuanyusong.com 
 */
 
[RequireComponent(typeof(CharacterController))]
 
public class ThirdPersonController : MonoBehaviour {
 
public AnimationClip idleAnimation ;
public AnimationClip walkAnimation ;
public AnimationClip runAnimation ;
public AnimationClip jumpPoseAnimation;
 
public float walkMaxAnimationSpeed  = 0.75f;
public float trotMaxAnimationSpeed  = 1.0f;
public float runMaxAnimationSpeed  = 1.0f;
public float jumpAnimationSpeed  = 1.15f;
public float landAnimationSpeed  = 1.0f;
 
private Animation _animation;
 
enum CharacterState 
{
	Idle = 0,
	Walking = 1,
	Trotting = 2,
	Running = 3,
	Jumping = 4,
}
 
private CharacterState _characterState;
 
// The speed when walking
float walkSpeed = 2.0f;
// after trotAfterSeconds of walking we trot with trotSpeed
float trotSpeed = 4.0f;
// when pressing "Fire3" button (cmd) we start running
float runSpeed = 6.0f;
 
float inAirControlAcceleration = 3.0f;
 
// How high do we jump when pressing jump and letting go immediately
float jumpHeight = 0.5f;
 
// The gravity for the character
float gravity = 20.0f;
// The gravity in controlled descent mode
float speedSmoothing = 10.0f;
float rotateSpeed = 500.0f;
float trotAfterSeconds = 3.0f;
 
bool canJump = true;
 
private float jumpRepeatTime = 0.05f;
private float jumpTimeout = 0.15f;
private float groundedTimeout = 0.25f;
 
// The camera doesnt start following the target immediately but waits for a split second to avoid too much waving around.
private float lockCameraTimer = 0.0f;
 
// The current move direction in x-z
private Vector3 moveDirection = Vector3.zero;
// The current vertical speed
private float verticalSpeed = 0.0f;
// The current x-z move speed
private float moveSpeed = 0.0f;
 
// The last collision flags returned from controller.Move
private CollisionFlags collisionFlags; 
 
// Are we jumping? (Initiated with jump button and not grounded yet)
private bool jumping = false;
private bool jumpingReachedApex = false;
 
// Are we moving backwards (This locks the camera to not do a 180 degree spin)
private bool movingBack = false;
// Is the user pressing any keys?
private bool isMoving = false;
// When did the user start walking (Used for going into trot after a while)
private float walkTimeStart = 0.0f;
// Last time the jump button was clicked down
private float lastJumpButtonTime = -10.0f;
// Last time we performed a jump
private float lastJumpTime = -1.0f;
 
// the height we jumped from (Used to determine for how long to apply extra jump power after jumping.)
private float lastJumpStartHeight = 0.0f;
 
private Vector3 inAirVelocity = Vector3.zero;
 
private float lastGroundedTime = 0.0f;
 
private bool isControllable = true;
 
void Awake ()
{
	moveDirection = transform.TransformDirection(Vector3.forward);
 
	_animation = GetComponent<Animation>();
	if(!_animation)
		Debug.Log("The character you would like to control doesn't have animations. Moving her might look weird.");
 
	/*
public var idleAnimation : AnimationClip;
public var walkAnimation : AnimationClip;
public var runAnimation : AnimationClip;
public var jumpPoseAnimation : AnimationClip;	
	*/
	if(!idleAnimation) {
		_animation = null;
		Debug.Log("No idle animation found. Turning off animations.");
	}
	if(!walkAnimation) {
		_animation = null;
		Debug.Log("No walk animation found. Turning off animations.");
	}
	if(!runAnimation) {
		_animation = null;
		Debug.Log("No run animation found. Turning off animations.");
	}
	if(!jumpPoseAnimation && canJump) {
		_animation = null;
		Debug.Log("No jump animation found and the character has canJump enabled. Turning off animations.");
	}
 
}
 
void UpdateSmoothedMovementDirection ()
{
	Transform cameraTransform = Camera.main.transform;
	bool grounded = IsGrounded();
 
	// Forward vector relative to the camera along the x-z plane	
	Vector3 forward = cameraTransform.TransformDirection(Vector3.forward);
	forward.y = 0;
	forward = forward.normalized;
 
	// Right vector relative to the camera
	// Always orthogonal to the forward vector
	Vector3 right = new Vector3(forward.z, 0, -forward.x);
 
	float v = Input.GetAxisRaw("Vertical");
	float h = Input.GetAxisRaw("Horizontal");
 
	// Are we moving backwards or looking backwards
	if (v < -0.2f)
		movingBack = true;
	else
		movingBack = false;
 
	bool wasMoving = isMoving;
	isMoving = Mathf.Abs (h) > 0.1f || Mathf.Abs (v) > 0.1f;
 
	// Target direction relative to the camera
	Vector3 targetDirection = h * right + v * forward;
 
	// Grounded controls
	if (grounded)
	{
		// Lock camera for short period when transitioning moving & standing still
		lockCameraTimer += Time.deltaTime;
		if (isMoving != wasMoving)
			lockCameraTimer = 0.0f;
 
		// We store speed and direction seperately,
		// so that when the character stands still we still have a valid forward direction
		// moveDirection is always normalized, and we only update it if there is user input.
		if (targetDirection != Vector3.zero)
		{
			// If we are really slow, just snap to the target direction
			if (moveSpeed < walkSpeed * 0.9f && grounded)
			{
				moveDirection = targetDirection.normalized;
			}
			// Otherwise smoothly turn towards it
			else
			{
				moveDirection = Vector3.RotateTowards(moveDirection, targetDirection, rotateSpeed * Mathf.Deg2Rad * Time.deltaTime, 1000);
 
				moveDirection = moveDirection.normalized;
			}
		}
 
		// Smooth the speed based on the current target direction
		float curSmooth = speedSmoothing * Time.deltaTime;
 
		// Choose target speed
		//* We want to support analog input but make sure you cant walk faster diagonally than just forward or sideways
		float targetSpeed = Mathf.Min(targetDirection.magnitude, 1.0f);
 
		_characterState = CharacterState.Idle;
 
		// Pick speed modifier
		if (Input.GetKey (KeyCode.LeftShift) | Input.GetKey (KeyCode.RightShift))
		{
			targetSpeed *= runSpeed;
			_characterState = CharacterState.Running;
		}
		else if (Time.time - trotAfterSeconds > walkTimeStart)
		{
			targetSpeed *= trotSpeed;
			_characterState = CharacterState.Trotting;
		}
		else
		{
			targetSpeed *= walkSpeed;
			_characterState = CharacterState.Walking;
		}
 
		moveSpeed = Mathf.Lerp(moveSpeed, targetSpeed, curSmooth);
 
		// Reset walk time start when we slow down
		if (moveSpeed < walkSpeed * 0.3f)
			walkTimeStart = Time.time;
	}
	// In air controls
	else
	{
		// Lock camera while in air
		if (jumping)
			lockCameraTimer = 0.0f;
 
		if (isMoving)
			inAirVelocity += targetDirection.normalized * Time.deltaTime * inAirControlAcceleration;
	}
 
}
 
void ApplyJumping ()
{
	// Prevent jumping too fast after each other
	if (lastJumpTime + jumpRepeatTime > Time.time)
		return;
 
	if (IsGrounded()) {
		// Jump
		// - Only when pressing the button down
		// - With a timeout so you can press the button slightly before landing		
		if (canJump && Time.time < lastJumpButtonTime + jumpTimeout) {
			verticalSpeed = CalculateJumpVerticalSpeed (jumpHeight);
			SendMessage("DidJump", SendMessageOptions.DontRequireReceiver);
		}
	}
}
 
void ApplyGravity ()
{
	if (isControllable)	// don't move player at all if not controllable.
	{
		// Apply gravity
		bool jumpButton = Input.GetButton("Jump");
 
		// When we reach the apex of the jump we send out a message
		if (jumping && !jumpingReachedApex && verticalSpeed <= 0.0f)
		{
			jumpingReachedApex = true;
			SendMessage("DidJumpReachApex", SendMessageOptions.DontRequireReceiver);
		}
 
		if (IsGrounded ())
			verticalSpeed = 0.0f;
		else
			verticalSpeed -= gravity * Time.deltaTime;
	}
}
 
float CalculateJumpVerticalSpeed (float targetJumpHeight)
{
	// From the jump height and gravity we deduce the upwards speed 
	// for the character to reach at the apex.
	return Mathf.Sqrt(2 * targetJumpHeight * gravity);
}
 
void  DidJump ()
{
	jumping = true;
	jumpingReachedApex = false;
	lastJumpTime = Time.time;
	lastJumpStartHeight = transform.position.y;
	lastJumpButtonTime = -10;
 
	_characterState = CharacterState.Jumping;
}
 
void  Update() {
 
	if (!isControllable)
	{
		// kill all inputs if not controllable.
		Input.ResetInputAxes();
	}
 
	if (Input.GetButtonDown ("Jump"))
	{
		lastJumpButtonTime = Time.time;
	}
 
	UpdateSmoothedMovementDirection();
 
	// Apply gravity
	// - extra power jump modifies gravity
	// - controlledDescent mode modifies gravity
	ApplyGravity ();
 
	// Apply jumping logic
	ApplyJumping ();
 
	// Calculate actual motion
	Vector3 movement = moveDirection * moveSpeed + new Vector3 (0, verticalSpeed, 0) + inAirVelocity;
	movement *= Time.deltaTime;
 
	// Move the controller
	CharacterController controller = GetComponent<CharacterController>();
	collisionFlags = controller.Move(movement);
 
	// ANIMATION sector
	if(_animation) {
		if(_characterState == CharacterState.Jumping) 
		{
			if(!jumpingReachedApex) {
				_animation[jumpPoseAnimation.name].speed = jumpAnimationSpeed;
				_animation[jumpPoseAnimation.name].wrapMode = WrapMode.ClampForever;
				_animation.CrossFade(jumpPoseAnimation.name);
			} else {
				_animation[jumpPoseAnimation.name].speed = -landAnimationSpeed;
				_animation[jumpPoseAnimation.name].wrapMode = WrapMode.ClampForever;
				_animation.CrossFade(jumpPoseAnimation.name);				
			}
		} 
		else 
		{
			if(controller.velocity.sqrMagnitude < 0.1f) {
				_animation.CrossFade(idleAnimation.name);
			}
			else 
			{
				if(_characterState == CharacterState.Running) {
					_animation[runAnimation.name].speed = Mathf.Clamp(controller.velocity.magnitude, 0.0f, runMaxAnimationSpeed);
					_animation.CrossFade(runAnimation.name);	
				}
				else if(_characterState == CharacterState.Trotting) {
					_animation[walkAnimation.name].speed = Mathf.Clamp(controller.velocity.magnitude, 0.0f, trotMaxAnimationSpeed);
					_animation.CrossFade(walkAnimation.name);	
				}
				else if(_characterState == CharacterState.Walking) {
					_animation[walkAnimation.name].speed = Mathf.Clamp(controller.velocity.magnitude, 0.0f, walkMaxAnimationSpeed);
					_animation.CrossFade(walkAnimation.name);	
				}
 
			}
		}
	}
	// ANIMATION sector
 
	// Set rotation to the move direction
	if (IsGrounded())
	{
 
		transform.rotation = Quaternion.LookRotation(moveDirection);
 
	}	
	else
	{
		Vector3 xzMove = movement;
		xzMove.y = 0;
		if (xzMove.sqrMagnitude > 0.001f)
		{
			transform.rotation = Quaternion.LookRotation(xzMove);
		}
	}	
 
	// We are in jump mode but just became grounded
	if (IsGrounded())
	{
		lastGroundedTime = Time.time;
		inAirVelocity = Vector3.zero;
		if (jumping)
		{
			jumping = false;
			SendMessage("DidLand", SendMessageOptions.DontRequireReceiver);
		}
	}
}
 
void  OnControllerColliderHit (ControllerColliderHit hit )
{
//	Debug.DrawRay(hit.point, hit.normal);
	if (hit.moveDirection.y > 0.01f) 
		return;
}
 
float GetSpeed () {
	return moveSpeed;
}
 
public bool IsJumping () {
	return jumping;
}
 
bool IsGrounded () {
	return (collisionFlags & CollisionFlags.CollidedBelow) != 0;
}
 
Vector3 GetDirection () {
	return moveDirection;
}
 
public bool IsMovingBackwards () {
	return movingBack;
}
 
public float GetLockCameraTimer () 
{
	return lockCameraTimer;
}
 
bool IsMoving ()
{
	return Mathf.Abs(Input.GetAxisRaw("Vertical")) + Mathf.Abs(Input.GetAxisRaw("Horizontal")) > 0.5f;
}
 
bool HasJumpReachedApex ()
{
	return jumpingReachedApex;
}
 
bool IsGroundedWithTimeout ()
{
	return lastGroundedTime + groundedTimeout > Time.time;
}
 
void Reset ()
{
	gameObject.tag = "Player";
}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值