当摄像机处于跟随主角状态时,那么主角移动后很有可能摄像机被别的模型挡住。这样用户体验就非常不好,一般3D游戏在处理视角的时候有两种方法,第一种是让被挡住的模型变成透明,第二种是拉近摄像机。前者时候固定视角游戏使用,后者适合变化视角游戏使用。两个我们都学一学蛤蛤。
如下图所示,MOMO写了一个简单的例子,通过鼠标或触摸来牵引主角向不同的角度移动,旁边有一面墙当主角被这面墙挡住,此时摄像机将拉近。
首先,为了方面鼠标与移动平台上的触摸同时相应我写了一个通用的方法。
包括 开始触摸 触摸中 结束触摸 ,电脑手机都可以用这个方法相应到。
JFConst.cs
using UnityEngine;
using System.Collections;
public class JFConst
{
public static bool TouchBegin()
{
if(Input.GetMouseButtonDown(0))
{
return true;
}
if(Input.touchCount > 0 && Input.GetTouch(0).phase == TouchPhase.Began)
{
return true;
}
return false;
}
public static bool TouchEnd()
{
if(Input.GetMouseButtonUp(0))
{
return true;
}
if(Input.touchCount > 0 && Input.GetTouch(0).phase == TouchPhase.Ended)
{
return true;
}
return false;
}
public static bool TouchIng()
{
if(Input.GetMouseButton(0))
{
return true;
}else if(Input.touchCount > 0 && Input.GetTouch(0).phase == TouchPhase.Moved)
{
return true;
}
return false;
}
}
以前写过一篇文章关于移动主角的,如果对移动主角还有不懂的请看这篇 Unity3D研究院之鼠标控制角色移动与奔跑示例(二十四) 。 这里我就不解释移动主角这部分的代码了。 下面是角色控制器ThirdPersonController.cs 绑定在主角身上。
下面代码中需要详细看的就是161行到174行 ,这部分就是根据触摸屏幕的2D坐标计算主角在3D世界中移动的坐标。
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 public float walkSpeed = 2.0f; // after trotAfterSeconds of walking we trot with trotSpeed public float trotSpeed = 4.0f; // when pressing "Fire3" button (cmd) we start running public float runSpeed = 6.0f; public float inAirControlAcceleration = 3.0f; // How high do we jump when pressing jump and letting go immediately public float jumpHeight = 0.5f; // The gravity for the character public float gravity = 20.0f; // The gravity in controlled descent mode public float speedSmoothing = 10.0f; public float rotateSpeed = 500.0f; public float trotAfterSeconds = 3.0f; public 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; if(JFConst.TouchIng()) { Vector3 vpos3 = Camera.main.WorldToScreenPoint(transform.position); Vector2 vpos2 = new Vector2 (vpos3.x,vpos3.y); Vector2 input = new Vector2 (Input.mousePosition.x,Input.mousePosition.y); if(Vector2.Distance(vpos2,input) > 10.0f) { Vector2 normalied = ((vpos2 - input)).normalized; targetDirection = new Vector3(normalied.x,0.0f,normalied.y) ; float y = Camera.main.transform.rotation.eulerAngles.y; targetDirection = Quaternion.Euler(0f,y - 180,0f) * targetDirection; } } // 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"; } }
如此我们就可以通过鼠标与触摸牵引控制主角移动了,别急这紧紧是开始,下面是本章的要点。这里我们分析一下摄像机到底什么时候该拉近,什么时候该不拉近。 我的作法是这样的,当角色移动的时候我会从主角身上向摄像机方向发射一条射线,如果射线碰撞到的第一个对象是“摄像机” 那么就标示主角和摄像机之间没有别的模型挡住,此时摄像机就不应该拉近。如果射线碰撞到的第一个对象不是摄像机,那么就标示主角和摄像机之间有别的模型所挡住,此时取得射线与别的模型碰撞的3D坐标接着将“摄像机”的坐标移动到这个坐标上即可。如下图所示,(红色表示由主角发射到摄像机的射线)主角身后的射线没有被别的模型挡住,射线机不会拉近。如下图所示,MOMO改变了一下主角的位置,此时主角身后的射向已经被墙挡住,这时把摄像机的坐标移动到被墙挡住点的坐标,这样摄像机就拉近了。原理大概就是这样,接着我们学习一下这段代码该如何来写。MyCamera.cs 挂在射线机上using UnityEngine; using System.Collections; public class MyCamera : MonoBehaviour { //摄像机朝向的目标模型 public Transform target; //摄像机与模型保持的距离 public float distance = 10.0f; //射线机与模型保持的高度 public float height = 5.0f; //高度阻尼 public float heightDamping = 2.0f; //旋转阻尼 public float rotationDamping = 3.0f; //主角对象 private GameObject controller; void Start() { //得到主角对象 controller = GameObject.FindGameObjectWithTag("Player"); } void Update() { } void LateUpdate () { // Early out if we don't have a target if (!target) return; //当鼠标或者手指在触摸中时 if(JFConst.TouchIng()) { bool follow = true; //计算相机与主角Y轴旋转角度的差。 float abs = Mathf.Abs(transform.rotation.eulerAngles.y - controller.transform.rotation.eulerAngles.y); //abs等于180的时候标示摄像机完全面对这主角, 》130 《 230 表示让面对的角度左右偏移50度 //这样做是不希望摄像机跟随主角,具体效果大家把代码下载下来看看,这样的摄像机效果很好。 if(abs > 130 && abs < 230) { follow = false; }else { follow = true; } float wantedRotationAngle = target.eulerAngles.y; float wantedHeight = target.position.y + height; float currentRotationAngle = transform.eulerAngles.y; float currentHeight = transform.position.y; //主角面朝射线机 和背对射线机 计算正确的位置 if(follow) { currentRotationAngle = Mathf.LerpAngle (currentRotationAngle, wantedRotationAngle, rotationDamping * Time.deltaTime); currentHeight = Mathf.Lerp (currentHeight, wantedHeight, heightDamping * Time.deltaTime); Quaternion currentRotation = Quaternion.Euler (0, currentRotationAngle, 0); Vector3 positon = target.position; positon -= currentRotation * Vector3.forward * distance; positon = new Vector3(positon.x,currentHeight,positon.z); transform.position = Vector3.Lerp(transform.position,positon,Time.time); }else { Vector3 positon = target.position; Quaternion cr = Quaternion.Euler (0, currentRotationAngle, 0); positon += cr * Vector3.back * distance; positon = new Vector3(positon.x,target.position.y + height,positon.z); transform.position = Vector3.Lerp(transform.position,positon,Time.time); } } //这里是计算射线的方向,从主角发射方向是射线机方向 Vector3 aim = target.position; //得到方向 Vector3 ve = (target.position - transform.position).normalized; float an = transform.eulerAngles.y; aim -= an * ve ; //在场景视图中可以看到这条射线 Debug.DrawLine(target.position,aim,Color.red); //主角朝着这个方向发射射线 RaycastHit hit; if(Physics.Linecast(target.position,aim,out hit)) { string name = hit.collider.gameObject.tag; if(name != "MainCamera" && name !="terrain") { //当碰撞的不是摄像机也不是地形 那么直接移动摄像机的坐标 transform.position = hit.point; } } // 让射线机永远看着主角 transform.LookAt (target); } }
牛头人被墙挡住了,摄像机直接拉近了,小牛头人是不是很帅气?哈哈哈!!最后雨松MOMO把源码放出来,希望大家学习愉快,哇咔咔。。。补充 :摄像机遮挡物体透明。遮挡透明最简单的办法就是修改材质的透明度,前提需要把材质设置成支持透明通道的 shader Transparent 。 如下图所示,摄像机挡住的那面墙已经成透明状态了。看看代码是如何写的,把上述代码MyCamera.cs 简单的改一下就可以。 主要是92行到101行。代码核心就是通过射线得到碰撞模型的材质,然后修改材质的透明度。color.a就是透明度 1是完全不透明,0是完全透明。注意一定是支持透明通道的模型才可以。lastobj是记录上次透明的对象,用于模型透明还原使用。if(Physics.Linecast(target.position,aim,out hit)) { GameObject obj = hit.collider.gameObject; string name = obj.tag; if(name != "MainCamera" && name !="terrain") { Color color = obj.renderer.material.color; color.a = 0.5f; obj.renderer.material.SetColor("_Color", color); lastobj = obj; }else { if(lastobj != null) { Color color = lastobj.renderer.material.color; color.a = 1f; lastobj.renderer.material.SetColor("_Color", color); } } }
最后,假设你的模型做的非常大,贴图材质也非常的多,你像透明其中一小部分那么上述方法就不合适了,除非把他们都拆出来。我觉得也可以自己写shader实现。我们的项目采用了摄像机拉近的方式。欢迎讨论!!!回答楼下问题:上帝视角代码,看到楼下有朋友问我,那么我就贴出来,其实很简单。下面代码挂在摄像机上, target就是角色对象,distance 是摄像机与角色的距离 height是摄像机与角色的高度。在编辑器中手动调节即可。using UnityEngine; using System.Collections; public class ICamera : MonoBehaviour { public Transform target; // The distance in the x-z plane to the target public float distance = 10.0f; // the height we want the camera to be above the target public float height = 5.0f; void LateUpdate () { // Early out if we don't have a target if (!target) return; // Set the position of the camera on the x-z plane to: // distance meters behind the target transform.position = target.position; transform.position -= Vector3.forward * distance; transform.position = new Vector3(transform.position.x,transform.position.y + height,transform.position.z); // Always look at the target transform.LookAt (target); } }
回答问题 :摄像机抖动如何修改今天微薄上有个朋友问我这样的问题,我在这里解答一下。来福12345:我又厚颜无耻的来了,您的《Unity3D的研究院之处理摄像机跟随避免相机穿墙拉近或透明的方法(四十四)》对于不规则的物体该怎么弄,我是直接在unity上弄了几个山脉,由于地面凹凸不平,沿着山走的时候发现摄像机会一直透视,而且抖动的很厉害,请问该怎么解决啊1.从设计上避免射线遇到一些复杂的面,如下图所示,红颜色是主角身后的射线,当主角身后的射线碰撞到这样的面,如果此时移动速快快一些摄像机肯定会抖动。 这里最好直接删除Mesh Collider 换成BoxCollider 效率还可以得到提升。产经设计这块我觉得可以参考一下魔兽世界,我在项目中遇到这样问题的地方就是 比较小的门(棱角那种) 或者这样的树。2.但是还有一些比较特殊的地方,如果你的项目必需使用MeshColider 的话 我建议你写触发脚本来修改摄像机,假设 当摄像机被挡住后,变成跟随视角,只到摄像机没被挡住的时候在回到原来视角。3.我检查了一下上面的代码中我们在加一个判断,应该就可以解决大部分问题、在LateUpdate () 中最后加入代码if(!closer) { transform.position = positon; } else { float dis = Vector3.Distance(transform.position,controller.transform.position); if(dis < 8) { distance = dis; }else { closer = false; } }
bool closer 是表示是否拉近。 当摄像机被挡住的时候这个数值等于true;
if(Physics.Linecast(tirggerTaget,aim,out hit)) { string name = hit.collider.gameObject.tag; if(name != "MainCamera") { closer = true; transform.position = hit.point; distance = Vector3.Distance(transform.position,controller.transform.position) - 2f; } }
当closer等于假的时候,此时直接修更新摄像机的位置,因为没被挡住。 当closer等于真的时候,摄像机已经被挡住了,我们不修改摄像机的位置,计算摄像机保证不被挡住时,摄像机此时和主角的距离是多少,然后修改distance全局变量即可。当摄像机不被遮挡的时候在恢复之前的距离。 如有问题请留言,我会即时解答。。 谢谢//补充/////////之前的代码还是有点问题,后来我又仔细的重构了一下摄像机的代码。下面的代码在我的项目中已经完美无暇的模拟摄像机拉近了。。 代码对外有两个公有类型, 一个是模型的 一个是模型位置 与头顶的偏移, 因为射线应该是重头顶向后发射的。using UnityEngine; using System.Collections; using System.Threading; public class PersonCamera : MonoBehaviour { public const float DISTANCE_DEAFULT = 8.0f; private float distance = 0.0f; public Transform target; public float target_offsety = 1.8f; private Vector3 payerTaget; void Awake() { distance = DISTANCE_DEAFULT; payerTaget = new Vector3(target.position.x,target.position.y + target_offsety , target.position.z); Quaternion cr = Quaternion.Euler (30f, transform.eulerAngles.y, 0); Vector3 positon = payerTaget; positon += cr * Vector3.back * distance; transform.position = positon; transform.LookAt(payerTaget); } void LateUpdate() { payerTaget = new Vector3(target.position.x,target.position.y + target_offsety , target.position.z); Quaternion cr = Quaternion.Euler (30f, transform.eulerAngles.y, 0); Vector3 positon = payerTaget + (cr * Vector3.back * distance); RaycastHit []hits = Physics.RaycastAll(new Ray(payerTaget,(positon -payerTaget).normalized)); distance = DISTANCE_DEAFULT; if(hits.Length > 0) { RaycastHit stand = hits[0]; foreach(RaycastHit hit in hits) { if(hit.collider.gameObject.tag == "terrain") { if(hit.distance < stand.distance) { stand = hit; } } } Debug.Log(stand.point + " " +stand.collider.gameObject.tag); string tag = stand.collider.gameObject.tag; distance = Vector3.Distance(stand.point,payerTaget); if(distance > DISTANCE_DEAFULT) { distance = DISTANCE_DEAFULT; } } positon = payerTaget + (cr * Vector3.back * distance); transform.position = Vector3.Lerp(transform.position,positon,0.3f);; transform.LookAt(payerTaget); Debug.DrawRay(payerTaget,positon -payerTaget,Color.red); } }