2013年9月16日 星期一

Unity3D研究院之处理摄像机跟随避免相机穿墙拉近或透明的方法(四十四)

http://www.xuanyusong.com/archives/1991

          当摄像机处于跟随主角状态时,那么主角移动后很有可能摄像机被别的模型挡住。这样用户体验就非常不好,一般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); 
 }
}