ai小车转弯 unity3d

2015-04-29 13:46:28 nijiayy 阅读数 3274
  • Unity 值得看的500+ 技术内容列表

    Unity3D是由Unity Technologies开发的一个让玩家轻松创建诸如三维视频游戏、建筑可视化、实时三维动画等类型互动内容的多平台的综合型游戏开发工具,是一个全面整合的专业游戏引擎。

ok,下面就进入我们的讲解工作啦,我的这个的讲解应该是比较适合初学者的,所以会有一些啰嗦什么的就请大家见谅啦!

想必来看这个blog的,大家都已经知道了Unity3D的一些基本的操作了吧!这里我也就不一一赘述了,毕竟不是什么一个Unity的课程。ok,我这里用的是Unity3D5,然后下面来说一下关于这个小程序的小车的建立。

一开始的时候,我只是在网上找了一些相关的3d模型然后直接拉入Unity中(注意一定要是fbx格式的哦),我当时天真的不断地调节车速和轮子的转动当然就是在脚本里写一些平移和旋转的代码啦,虽然离近了看还是不真实,但是至少我们的车轮还是动了是吧~但是,当遇到拐弯的时候,我就遇到了瓶颈,如果一个正常的车,它的拐弯一定是车头引导的啊,但是我现在无论怎么变也只是看着车头和车尾一起平移过去的,于是我开始查找,找了很多资料(或许也是刚学,没有输入正确的关键词吧),终于让我发现了一个unity自带的一个函数库——WheelCollider,这个类真的是天生就是为赛车游戏开发而设立的啊!这里的运动完全是通关后轮的驱动还带动前轮(当然也可以四驱),而拐弯也就是前轮的带动,实在是完美的解决了我的问题,但是问题也就随之而来了,那就是怎么用这个?我找了好多的资料,都是说把这个库拖入轮子上就好,但是,我拖入后根本就是无法和预想的一样啊,后来我好不容易发现了一个教学视频真的是帮了我的大忙!我觉得文字还是不够直观,所以我觉得大家还是去看看这个视频,真的很有用,那位老师教的很好~这里给出一个链接,大家可以去看看(一共分为了五个你可以找一下车轮的设置在哪里)可以到这里学习:http://www.tudou.com/programs/view/CaBwp072c2Y/

ok 下面就说一下具体的实现吧,话说让车子拐弯而又不脱离公路哪里我还真的是研究了好半天,怎么也找不到方法,终于想起了下载Unity自带的例子,于是我就去找,还真的找到了一个有关汽车ai的例子,也是千辛万苦的读下来,但是其中很多算法还是不明白,但是我发现我已经找到了一个方法了,而且是不需要ai的。受到例子的启示,我本来是想直接还是用Unity的一个WayPoint Circuit的但是这个是建立环路的,而我的想法是建立一条单向道路,所以我就防着这样的思路,建立了一个我的汽车行进的路线,如下图所示:


看到啦那个way_1那些标志了吗?这就是我们的路标点,其实就是一个个的Cube然后把他们保留Collider但是把形状渲染取消,所以就成了一个个透明的小盒子,给他们加入触发器,当校车碰到这个标志点的时候就让小车指向下一个标志点,到达最后一个标志点的时候把这个校车Destory,由于我们的路标点很少只有五个,如果让校车驶向一个个的路标点的话那么必然就会导致小车行驶的十分生硬,所以受到了Unity自带例子的启发我给没一个小车前面加入了一个透明的引导球这样的话让引导球驶向标志点,小车追着引导球(当然小车和引导球之间保持一样的速度),引导球就起到了一个缓冲的作用,保证了小车的圆滑的拐弯。下面是一个小车整体的组成:


看到了左边的小球了吗?那个除了引导的作用还有探测前方是否有车的情况,如果它有碰撞的话那么就会出现一个汽车被它后面的另一个汽车离得好远撞飞了,所以不可能让它具有碰撞性,所以,我们就应该给他设置为触发器方式,但是再次运行的时候你就会发现,这个引导球不见了,小车也不按照既定的路线行驶了,我通过种种的实验发现需要把小球的重力取消就ok!


这个是小球的检视面板看那个IsTriger打了勾就代表这个小球无碰撞,是一种触发器模式,而那个Use Gravity没有打勾就是没有重力的影响。而那个ballmove就是我们要写的函数,小球有两个任务一个是引导,一个是检测车辆,下面给出代码:

#pragma strict
private var waypoint:Transform[]=new Transform[5];
private var move_speed:float;
private var nextpoint:int;
var car:GameObject;
private var carbody:Rigidbody;
private var path:GameObject;
private var current:int;//记录当前point位置
private var turn_brake:int;//转弯刹车
private var stop_brake:int;//停车刹车
private var way_name:String;
private var go:int;
private var last_colllider:String;
function Start () {
  turn_brake=20;//30
  stop_brake=10000;
  nextpoint=0;
  carbody=car.GetComponent("Rigidbody");
  way_name=createcar.path_name;//从生成车辆函数得到当前车的位置并且选择与之对应的道路
  Debug.Log(way_name);
  if(way_name==null){//第一辆车本就有的不用生成所以对应name是null
    way_name="left_right";
  }
  switch(way_name){//针对不同位置生成的车对应不同的行进方式
    case "left_right":
    path=GameObject.Find("way_left_turnright");
    break;

    case "left_left":
    path=GameObject.Find("way_left_turnleft");
    break;
    
    case "left_go":
    path=GameObject.Find("way_left_go");
    break;
    
    case "left_hurry":
    path=GameObject.Find("way_left_hurry");
    break;
    
    case "bot_right":
    path=GameObject.Find("way_bot_right");
    break;
    
    case "bot_left":
    path=GameObject.Find("way_bot_left");
    break;
    
    case "bot_go":
    path=GameObject.Find("way_bot_go");
    break;
    
    case "bot_hurry":
    path=GameObject.Find("way_bot_hurry");
    break;
    
    case "top_right":
    path=GameObject.Find("way_top_right");
    break;
    
    case "top_left":
    path=GameObject.Find("way_top_left");
    break;
    
    case "top_go":
    path=GameObject.Find("way_top_go");
    break;
    
    case "top_hurry":
    path=GameObject.Find("way_top_hurry");
    break;
    
    case "right_right":
    path=GameObject.Find("way_right_right");
    break;
    
    case "right_left":
    path=GameObject.Find("way_right_left");
    break;
    
    case "right_go":
    path=GameObject.Find("way_right_go");
    break;
    
    case "right_hurry":
    path=GameObject.Find("way_right_hurry");
    break;
    
  }
  //childCount获得子物体的数量
  for(var i:int =0;i<path.transform.childCount;i++){//遍历路径的父集遍历集合把对应的point点放入我们的位置点数组中
    //Debug.Log(path.transform.GetChild(i).name);
    waypoint[i]=path.transform.GetChild(i);
  } 
}

function Update () {
  move_speed=carbody.velocity.magnitude;
  //Debug.Log("ball:"+move_speed);
  //改变小球的移动方向
  transform.forward=waypoint[nextpoint].position-transform.position;
  transform.Translate(0,0,move_speed*Time.deltaTime);
}

function lastcollider(name:String){
   Debug.Log("last coiilder:"+name);
   last_colllider=name;
}

function OnTriggerEnter(go:Collider){
//碰撞到point后转向下一个point
if(go.transform.name=="way_1"||go.transform.name=="way_2"||go.transform.name=="way_3"||go.transform.name=="way_4"||go.transform.name=="way_5"||go.transform.name=="stop_line"||go.transform.name=="Terrain"){
   counterway(go);
 }else if(transform.name=="cop_target"&&go.transform.name=="cop_collider"){//警车自己不要把自己弄停
   
 }else if(go.transform.name=="cop_target"||go.transform.name=="target"){
 
 }
 else if(go.transform.name!=lastcollider){
  Debug.Log("car_stop:"+go.transform.name);
  gameObject.SendMessageUpwards("isgo",1);
 }
}
function OnTriggerExit (other : Collider ) {
 // Debug.Log("exit");
  gameObject.SendMessageUpwards("isgo",0);
}

function counterway(go:Collider){
 if(nextpoint<waypoint.Length&&go.transform.IsChildOf(path.transform)){//因为有很多引导块,所以这里片段一下各自的路线,不要
 // 误走过了其他的路径块而导致计数的增加
   nextpoint++;
  }
 //右转遇到第二个标志点就减速
  if(way_name=="left_right"||way_name=="right_right"||way_name=="top_right"||way_name=="bot_right"){
  if(nextpoint==2){
    //Debug.Log("slow");
    gameObject.SendMessageUpwards("isbrake",turn_brake);
   }else{
    gameObject.SendMessageUpwards("isbrake",0);
   }
  }
  if(nextpoint==waypoint.Length){
   //Debug.Log("destory");
   Destroy(car);
  }
}
这里都有相应额注释我相信大家应该都可以看懂,其实代码并不难,重要的是一个解决问题的方式,ok小球和路径的建立就说到这里~

对了,想必大家也都看到了我有很多需要确定字符串的地方例如:way_name=="left_right"其实这里的字符串是要和建立物体的命名取得一致的,如下图给一个例子:


ok,今天这一节就讲到这里啦~

2018-05-18 18:29:47 Scopperil 阅读数 1231
  • Unity 值得看的500+ 技术内容列表

    Unity3D是由Unity Technologies开发的一个让玩家轻松创建诸如三维视频游戏、建筑可视化、实时三维动画等类型互动内容的多平台的综合型游戏开发工具,是一个全面整合的专业游戏引擎。

    前段时间使用官方的例子来实现小车寻路,虽然具体功能实现了,但是有一些问题,比如小车在点与点的转换时转向十分突兀,而且需要很多点才能实现一个转弯,而且很容易与其他物体发生碰撞。且无法实现动态避障。

    在这些问题的基础上,我不得不重新思考寻路问题,后来还是使用unity自带模块navigation寻路模块以及射线映射基本上解决了所有问题,具体思路是:

    首先,选择路径进行烘培,烘焙的路径将作为可行区域。设置一个目标点,和前面思路一样,设置多个路径点切换。不同的是,小车会借助navigation模块自动在可行区域寻找最优路线,并借助射线来平滑计算需要调整的角度,当然需要在转弯处设置制动带,这样不会因为小车行驶速度过快发生事故,这样大大减少我们前面要设置很多很多个点才能实现转弯功能。这样就可以实现自动寻路了。

    其次,我们要实现避开行人,我使用的是射线检测的方式,车辆前方添加一组扇形区域的射线,它会碰撞到物体时给你反馈,当是people时,计算距离,当小于多少时自动刹车待行人通过继续行走,若一段时间还是无法通过,则选择绕行其他可行区域。还需要实现红路灯的功能,这个就比较简单,利用navigation的动态壁障就可以实现,类似机关。给某段路径添加障碍物的组件,并使用脚本使它在一段时间内setActive,这样就可以达到效果。

    unity中的AI可以简单可以复杂,对于相对目标行为的靠近、离开、抵达、追逐、逃避、随机徘徊等,对于AI角色对游戏世界的感知,自主决策(有限状态机),复杂决策(行为树)等,我会继续研究。

    我在自己搭建路径的时候,由于是网上找的一节一节的道路,run的时候在道路接口处发生明显的抖动,这个bug需要研究研究。

    具体请参考GitHub中AIcar2部分。

2018-04-04 16:24:00 weixin_30484247 阅读数 60
  • Unity 值得看的500+ 技术内容列表

    Unity3D是由Unity Technologies开发的一个让玩家轻松创建诸如三维视频游戏、建筑可视化、实时三维动画等类型互动内容的多平台的综合型游戏开发工具,是一个全面整合的专业游戏引擎。

Unity 2-7 Stealth秘密行动

Abstract:向量运算;Animation动画;Navigation寻路系统;Mecanim动画系统

任务1&2&3:游戏介绍
&& 创建工程和游戏场景介绍 && 创建游戏环境

逃生游戏,过关条件为拿到钥匙并从电梯处逃脱
被敌人/ 摄像头/ 触碰红外线 -- 触发警报
红外线可以手动断电

右上图场景是美工创建好的
  Import package->Custom package: StealthAssets.unitypackage

资源介绍:
  Animation, Audio, Fonts, Gizmos, Materials, Models, Shaders, Textures

Animation中的.fdx文件是从3d软件中导出的动画模型
humanoid动画可以应用在任意人形模型上

Audio:声音文件
Fonts:字体
Gizmos:waypoint的图标,用于敌人寻路的AI,用图标表示路径
Materials:材质
Models:模型(环境模型、物品模型等)
  里面有一个文件夹Collision Meshes,存放collision的mesh,用于碰撞检测
  比如prop_cctvCam_collision: 一个椎体,用来检测player是否进入了摄像头视野
Shaders:更好的效果
Textures:模型的贴图

创建游戏环境:
  1. 新建文件夹Scenes,保存场景Stealth
  2. 创建空物体env,用于存放有关环境的物体
  3. 将Models->env_stealth_static拖入场景(env下)
    Reset Transform
  4. 给env_stealth_static添加碰撞器
    添加Mesh Collider
    在Mesh属性中指定简化的Models->Collision Meshes:
      env_stealth_collision->env_stealth_collision_001
    在scene模式下,可以看到的绿色网格就是刚才的collision

创建battleBus:
  发现中间一块有一个突起的mesh collider,而没有物体存在
  由于在env_stealth_static中没有放入位于地图中间的小车

因此手动添加prop_battleBus到地图内 (作为env子物体),并旋转到车头朝向杂物,位置与Mesh Collider重合即可

任务4:添加环境灯光

给环境添加环境灯光:
  在最外层创建空物体light (位置归零),用于存放所有灯光
  在light中创建Directional Light,颜色为暗一点的灰色,Intensity调小(0.1)
  Main Camera背景颜色调为黑色,Clear Flags调为Solid color

给房间添加灯光:
  在light中创建Point Light,颜色为暗橘黄色,
  一个房间添加一个,在四周的墙上或过于阴暗的地方放置若干

任务5&6:添加警报灯 && 添加警报声

实现警报灯
添加Directional Light: AlarmLight,红色,触发警报的时候Intensity在0.1~0.5之间不断变化
在AlarmLight上添加脚本AlarmController.cs

public bool isAlarmOn = false; // Flag
private float lowest/highestIntensity = 0.1/ 0.5; // 指定最高值最低值
private float targetIntensity; // 当前需要往lowest还是highest靠近

在Awake()中:
  targetIntensity = highestIntensity; // 默认是从lowest变到highest的
  isAlarmOn = false; // 确保刚开始警报未触发

在Update()中实现警报灯闪烁的效果:
  if(isAlarmOn) { // 警报开启状态
  light.intensity=Mathf.Lerp(ligh.intensity, targetIntensity, Time.deltaTime*speed);

现在的警报实现了从lowestIntensity到highestIntensity的变亮过程
  判断接近 (Lerp是一个无限趋近的过程) targetIntensity后,改变target的值

  if(Mathf.Abs(light.intensity - targetIntansity) < 0.05f) { // 达到目标值
    if(targetIntensity == highestIntensity) {
    targetIntensity = lowestIntensity;
  } else { 同理; }

当警报消除的时候,需要将Intensity设为0(直接设置会显得不自然,采用Lerp方法)
  light.intensity = Mathf

但是(个人认为)这么写需要一直执行intensity的赋值操作

为了让外界访问AlarmController.cs方便,采用单例模式
  public static AlarmController _instance;
  在Awake()中:_instance = this;
  在外界直接访问_instance即可

public class AlarmController : MonoBehaviour {
    public static AlarmController _instance;

    public bool isAlarmOn = false;
    private Light alarmLight;
    private float lowestIntensity = 0.1f;
    private float highestIntensity = 0.75f;
    private float targetIntensity;
    private float currentIntensity;
    private float intensityChangeSpeed = 5f;

    void Awake () {
        _instance = this;
        targetIntensity = highestIntensity;
        currentIntensity = lowestIntensity;
        isAlarmOn = false;
        alarmLight = GetComponent<Light>();
    }

    void Update() {
        if (isAlarmOn) {
            if (Mathf.Abs(currentIntensity - targetIntensity) < 0.05) {
                if (targetIntensity.Equals(highestIntensity)) {
                    targetIntensity = lowestIntensity;
                } else {
                    targetIntensity = highestIntensity;
                }
            }
            currentIntensity = Mathf.Lerp(currentIntensity, targetIntensity, 
                Time.deltaTime * intensityChangeSpeed);
        } else {
            // 警报处于关闭状态
            currentIntensity = Mathf.Lerp(currentIntensity, 0, 
                Time.deltaTime * intensityChangeSpeed);
        }
        alarmLight.intensity = currentIntensity;
    } 
}

实现警报声效果:

在env_stealth_static模型里会找到若干个喇叭: prop_magaphone
对这六个喇叭添加AudioSource组件,用于播放声音 alarm_triggered
取消勾选play on awake
将Min Distance设置大一点 (5)
之后需要播放警报声时,需要找到这六个喇叭,因此把他们设置为tag=Siren

任务7:游戏控制器GameController -- 控制整个游戏的运行

游戏控制器:
  灯光、声音、警报主角位置(比如主角位置暴露了,需要记录看到主角的最新位置,让机
  器人朝着最新位置移动)

创建空物体GameController,添加GameController.cs脚本

需要控制警报灯:
  public bool isAlarmOn = false;
  在Update()中将isAlarmOn传递给任务6中的单例模式AlarmController
    // 因为要随时控制isAlarmOn的值
    AlarmController._instance.isAlarmOn = this.isAlarmOn;

需要控制警报声:
  // 得到六个警报喇叭
  private GameObject[] sirens = GameObject.FindGameObjectsWithTag("Siren");
两个方法:分别控制警报声的响起和停止
  private void PlaySiren() {
    foreach (GameObject siren in sirens) {
      if(!siren.audio.isPlaying) { // 如果没有播放
        siren.audio.Play(); // 新版unity改用GetComponent AudioSource
  }}}
  private void StopSiren() { // 相似,但是不需判断当前是否正在播放 }
在Update()中,控制警报声的播放和停止
  if(isAlarmOn) {
    PlaySiren();
  } else {
    StopSiren();
  }

public class GameController : MonoBehaviour {
    public bool isAlarmOn = false;
    private GameObject[] sirens;

    private void Awake() {
        isAlarmOn = false;
        sirens = GameObject.FindGameObjectsWithTag("Siren");
    }

    private void Update() {
        AlarmController._instance.isAlarmOn = this.isAlarmOn;
        if (isAlarmOn) {
            PlaySiren();
        } else {
            StopSiren();
    }}

    private void PlaySiren() {
        foreach (GameObject siren in sirens) {
            if (!siren.GetComponent<AudioSource>().isPlaying) {
                siren.GetComponent<AudioSource>().Play();
    }}}

    private void StopSiren() {
        foreach (GameObject siren in sirens) {
            siren.GetComponent<AudioSource>().Stop();
    }}
}

任务8&9:实时摄像机CCTV Camera && 摄像机的自动旋转
任务10:摄像机的警报触发功能

在Prefab中可以找到 prop_cctvCam

在环境中需要有三个cctv Camera,分别在

创建空物体Camera,用于放置所有cctvCamera

第一个camera放在bus的两桶油下方
  摄像机的旋转(Joint)通过x和y的旋转实现,设置为低头60°
给摄像机添加灯光
  在cctv_cam_body上添加Light组件,Light设为Spot(探照灯)
  Light的cookie设置为texture: fx_cameraView_alp,颜色改为红色
将cctv_collision添加进cctv_cam_body,调整位置
  取消cctv_collision的renender渲染
  给collision添加mesh collider碰撞器,用于碰撞检测
将上面的cctv_Camera做成prefab

将其他两个camera分别放置

摄像机的旋转

通过Animation的方式,控制joint部分的y轴旋转

1. 新建Animator -- Window->Animation->Create CameraSweepAnimation.anim

2. Add Property: Transform->Rotation

3. 在对应时间点上添加KeyFrame,并设置Rotation.y的值(90~0~-90)
  Sample为每秒的帧数

4. 因为左下角的摄像机不需要进行旋转,因此不能apply to prefabs
  在另一个需要旋转的摄像机上添加Animator组件,并赋值,即可

摄像机的警报触发:

思路:在左下角摄像机中完成警报触发功能,并apply to prefabs

1. 在prop_cctvCam的collision子物体上添加脚本CctvCamCollision.cs

2. 将碰撞器设置为Trigger,因为不需要有物理碰撞效果

3. OnTriggerEnter(Collider other) {}
  if(other.tag.Equals("Player")) { // 触发警报 }

4. 触发警报需要设置GameController.cs中的isAlarmOn
  GameController设置为单例模式
  public static GameController _instance;
  _instance = this;

设置isAlarmOn:
  GameController._instance.isAlarmOn = true;

5. 需要记录当前警报触发位置
  在GameController.cs中
  public Vector3 lastPlayerPos;
  GameController._instance.lastPlayerPos = other.transform.position;

6. 使用OnTriggerStay()更好,因为当Player在其中移动的时候,会触发位置更新

任务11:标签的管理(代码管理)

使用代码进行标签的管理(使用字符串的过程中很可能出现字符串打错等情况)

创建脚本Tags.cs
  // 注意:Tags不是作为一个组件存在的,只是存放了一些变量

public const string player = "Player";
  // const -- 常量(tags不需要修改)

使用的时候:
  if(other.tag == Tags.player) {}

还有其他tags:
  "Siren"、"Enemy"等

任务12&13:添加激光警报装置 && 警报的触发和闪烁

fx_laserfence_laser: 
  调整大小、角度和位置(门柱上正好有孔,与laser一一对应)

1. 给laser添加Collider组件,用于碰撞检测 -- BoxCollider,选择Trigger

2. 给laser添加light组件,发光:PointLight、范围变小、红色、强度增大

3. 给laser添加Audio Source组件: Audio->laser_ham,发声:
Spetial Blend、min/max distance、PlayOnAwake、Loop

4. 做成Prefab

5. 创建空物体lasers,放入另外五个激光警报(一共6个)即可

激光警报的触发:

添加脚本LaserController.cs

OnTriggerStay() {
  // 和摄像头触发警报一样的操作
  // 因此在GameController中写成函数public void SwitchAlarmOn(Transform t)
  if(tag....) GameController._instance.SwitchAlarmOn(other.transform);

Apply to prefabs.

激光警报的闪烁:

两个激光(最长的那两条)需要间隔闪烁,方便Player的通过

在LaserController中:

public bool isFlicker;
public float onTime = 3f;
public float offTime = 1.5f;
private float timer = 0;

private void Update() {
    if(isFlicker) {
        if(this.gameObject.GetComponent<Renderer>().enabled) {
            // 当前亮着
            timer += Time.deltaTime;
            if(timer >= onTime) {
                this.gameObject.GetComponent<Renderer>().enabled = false;
                timer = 0;
            }
        } else {
            // 当前暗着
            timer += Time.deltaTime;
            if(timer >= offTime) {
                this.gameObject.GetComponent<Renderer>().enabled = true;
                timer = 0;
}}}}

(Collider也需要禁用和启用) --  
  gameObject.GetComponent<BoxCollider>().enabled;

在Inspector中将两个需要闪烁的激光的isFlicker属性设置为true
两个激光闪烁不同步,一个为(1.6, 2.8), 一个为(2.1, 2.5)(随意)

任务14&15:主角和主角的动画

添加主角:

Models->char_ethan

给Player添加碰撞器Capsule Collider

给Player添加刚体Rigidbody
  因为主角不需要通过rigidbody进行移动(使用动画控制),所以勾选IsKinematic
    (在任务16中会发现这是一个bug)
  主角在(x, z)平面上移动,且只围绕y轴旋转,所以Freeze Position: y; Rotation: x, z

主角移动的动画:

在Humanoid中的动画是人形动画,可以使用

新建一个Animator Controller,名为PlayerController

打开Animator编辑器(选中PlayerController,Window->Animator)

1. 主角的状态:
  walk/ run (walk和run是同一个动作,只不过速度不同 -- 相同动画)
  sneaker
  idle
  death

2. 添加parameter
  float: speed -- 移动速度
  bool: sneaker -- 是否处于sneaker状态(按下shift键)

3. 默认状态:idle
  将humanoid_idel->Idle动画拖入状态机(黄色为默认动画)

4. 右键Create State -> From Blend Tree
  (上面提到walk和run是同一个动作,用BlendTree实现)
  命名为Locomotion (移动、运动)
  双击进入编辑模式
  Add Motion Field,添加Walk和Run动画
  在Parameter中选择speed,用speed来控制walk和run的融合
  uncheck Automate Thresholds (自动生成参数)
    取消勾选后可以手动确定:参数是根据哪个值来确定的,这里选择speed
    
    这里表示1.555在Walk,speed增大渐渐进入run的状态,5.667为run状态

5. Idle->Locomotion
  make transition: Conditions: speed > 0.1
  Locomotion->Idle: speed < 0.1

6. Sneak: humanoid_sneak: sneak动画拖入状态机
  Idle->Sneak:
    make transition: Conditions: speed > 0.1 && sneak == true;
  Sneak->Idle: speed < 0.1

7. Locomotion->Sneak:
  speed > 0.1 && sneak == true;
  Sneak->Locomotion:
  speed > 0.1 && sneak == false;

由于player的移动是通过动画实现的
  因此如果觉得移动过慢/快,可以修改对应状态的speed

8. Dying: humanoid_dying->Dying
  添加parameter: bool dead
  因为任何状态下都会死亡 --
    AnyState->Dying: make transition: dead = true;

任务16&17:控制主角的运动和移动

运行游戏,手动调整主角的速度,会发现主角出现了移动,主角的transform也发生变化
  由于Player->Animator勾选了属性Apply Root Motion,表示动画会影响transform的值

思路:按下上下左右键时,设置speed即可
  根据按键控制朝向

在char_ethan添加脚本PlayerController.cs

在Update()中实现:

// 得到按键的信息
float h = Input.GetAxis("Horizontal");
float v = Input.GetAxis("Vertical");

// 需要得到Animator来改变speed的值
private Animator anim = GetComponent...

(我的思路)
  anim.SetFloat("Speed", (Mathf.Max(h,v) * 5.56f));
  -- 结果为不适合
    1. 数值变化太快,walk->run过于迅速
    2. 而且延迟较大,Input.GetAxis()的值需要时间来归零和增大

(正确思路 -- 利用差值)
  if(Mathf.Max(Mathf.Abs(h), Mathf.Abs(v)) > 0.1f) {
    // 开始移动
    newSpeed = Mathf.Lerp(anim.GetFloat("speed"), maxSpeed, delta...)
  } else { // 停止移动
    // 相同思路,利用差值,但是将speedRate增大,因为需要很快停止移动
    // 试验过后发现还是不好控制,于是直接将speed设为0
    // 但是从跑步到Idle的动画很不平滑 -- 自行选择
    newSpeed = 0;
  }
  anim.SetFloat("speed", newSpeed);

出现bug -- Player会进行穿墙,就好像Collider没有任何作用一样
  原因:任务14中给Player添加Rigidbody时勾选了IsKinemetic选项,取消勾选即可

控制Player的移动方向:
  在需要移动的if条件中:
    // 取得目标方向
    Vector3 targetDir = new Vector3(h, 0, v);
    // 取得当前方向
    Vector3 currDir = transform.forward;
    // 取得两个方向的夹角
    float angle = Vector3.Angle(targetDir, currDir);
    // 使用匀速旋转的方法
    transform.Rotate(Vector3.up * angle * Time.deltaTime * rotateSpeed);
    // rotateSpeed可设为5

到此为止player的移动功能实现了,但是
  出现Bug --

1. 角色在停止控制后有的时候会继续不听旋转
  (个人推测是惯性?)
  将角色的Rotation: y Freeze即可

2. 拐弯的时候会出现有的时候往大弯绕
  比如:角色朝左,向下控制,角色会通过上右下而向下移动
     angle的值也是从90变大到180再变小
     细心观察会发现,角色永远是顺时针旋转
  原因:Vector3.Angle()返回值没有正负,小于180°
     Rotate(...)方向为Vector3.up,因此永远是顺时针旋转
       而旋转一些以后,angle值变大,需要继续旋转,直到angle值为0
  解决方法 -- 想不出如何判断使用Vector3.up还是Vector3.down
    舍弃该方法
    使用Quaternion和Slerp来解决
    Quaternion targetQuaternion = Quaternion.LookRotation(targerDir);
    // 四元数返回的是一个带有方向的角度 (该函数默认以Vector3.up为上方)
    transform.rotation = Quaternion.Slerp(transform.rotation,
      targetQuaternion, Time.deltaTime * rotationSpeedRate);

private Animator animator;
private float increaseSpeedRate = 2.5f;
private float decreaseSpeedRate = 60f;
private float rotateSpeedRate = 2;
private float newSpeed;
private const float maxSpeed = 5.66f;

private void Awake() {
    animator = GetComponent<Animator>();
}

void Update() {
    float h = Input.GetAxis("Horizontal");
    float v = Input.GetAxis("Vertical");
    if (Mathf.Max(Mathf.Abs(h), Mathf.Abs(v)) > 0.1f) {
        // 开始移动
        newSpeed = Mathf.Lerp(animator.GetFloat("speed"), maxSpeed, 
            Time.deltaTime * increaseSpeedRate);
        // 开始旋转
        // 取得目标方向
        Vector3 targetDir = new Vector3(h, 0, v);
        Quaternion targetDirQuaternion = Quaternion.LookRotation(targetDir);
        transform.rotation = Quaternion.Slerp(transform.rotation, 
            targetDirQuaternion, Time.deltaTime * rotateSpeedRate);
    } else {
        // 停止移动
        newSpeed = Mathf.Lerp(animator.GetFloat("speed"), 0, 
            Time.deltaTime * decreaseSpeedRate);
        // newSpeed = 0;
    }
    animator.SetFloat("speed", newSpeed);
}

实现sneak缓慢行走:

if(Input.GetKeyDown(KeyCode.LeftShift)) {
  anim.SetBool("sneak", true);
}
if(Input.GetKeyUp(KeyCode.LeftShift)) {
  anim.SetBool("sneak", false);
}

会发现,从Idle时按住shift后再按方向键,刚开始一小小段时间会进行walk而不是sneak
解决方法 --
  将动画状态机中Idle->Locomotion的condition加上sneaker=false;即可

此时sneak的速度有点慢
  控制sneak的动画播放速度即可
  选中动画状态机中的sneak,将inspector中的speed加大即可

任务18:主角行走声音和游戏背景音乐

添加背景音乐:

在GameController中添加AudioSource: music_normal,PlayOnAwake,Loop,2D

添加行走声音:

在主角上添加AudioSource: player_footsteps

在PlayerController.cs中添加两个方法

void PlayFootstepsAudio() {
  if(!audio.isPlaying) {
    audio.Play();
}}

void StopFootstepsAudio() {
  audio.Stop();
}

在Locomotion的状态下需要播放声音,其他状态不要

if(anim.GetCurrentAnimatorStateInfo(0).IsName("Locomotion")) {
  // play audio
} else {
  // stop audio
}

切换背景音乐:

当警报被触发的时候,背景音乐会从music_normal切换至music_panic
思路:如果直接暂停播放会显得比较突兀,因此使用对两个Volume进行差值运算的方法

在GameController上添加另一个AudioSource: music_panic, Loop

在GameController.cs中:
  定义两个AudioSource来存放两个声音
  在Update()中修改两个AudioSource的Volume
    在原有的isAlarmOn判断下
    musicNormal.volume=Mathf.Lerp(musicNormal.volume, 0, Time.deltaTime);
    musicPanic.volume=Mathf.Lerp(musicPanic.volume, 1, Time.deltaTime);
    !isAlarmOn时也类似
  也可以添加一个musicFadeSpeed来控制渐变速度
  Panic的背景声音有点大,因此改为0.5f

任务19:添加自动门

自动门机制:当主角靠近门的时候会门会自动打开: door_generic_slide
  门的模型都自带两个动画 open和close

创建空物体,用于存放所有门

使用Animator状态机进行门的控制
  新建Animator,名为normalDoorAnimationController
  将两个动画拖入状态机 -- 默认为close状态,设为default
  添加Parameters: bool closing; // 当closing为true时,进行关闭,为false时进行开启
    closing初始值为true
  添加状态close和open之间的transition

给door_generic_slide->door_generic_slide_panel 添加BoxCollider来检测碰撞
给door_generic_slide添加sphere collider,用于trigger开门动画

在door上添加DoorController.cs脚本 -- 代码控制门的开关
  因为是控制门的开关,因此如果是Enemy或Player在触发区内,保持开门状态
  OnTriggerStay(Collider other) {
    if(other.tag == Tags.Player || ... == Tags.Enemy) {
      anim.SetBool("closing", false);
  }}
  // 那如何关门呢?OnTriggerExit()? 不行,如果区域内有两个人的话,怎么解决?
  // 使用count来计数 -- 就不能用OnTriggerStay了
  OnTriggerEnter(...) {
    if(other.tag ... || ... ) {
      count++;
  }}
  OnTriggerExit(...) { // 相同情况 count--; }

  在Update中用count来判断是否开关门
            doorAnimator.SetBool("closing", (count<=0));

添加开关门的声音:打开和关闭时播放声音
  在门上添加AudioSource: door_open
  在Update中更改doorAnimator.closing时
    // 播放声音
    if(anim.IsInTransition(0) {
      // 如果在0 layer中正在进行某个Transition
      audioDoorOpen.Play();

将门做成Prefab,并创建其他两扇门

任务20:添加电梯门 -- 设置内侧外侧打开和关闭动画

电梯的自动门是两扇,往两边打开
  电梯门分为外侧和内侧两扇门(内侧门即电梯,跟随上下移动的)

添加外侧门:door_exit_outer

相同的,分别给door_exit_outer_left和right添加上BoxCollider
  给door_exit_outer添加上SphereCollider作为Trigger
  AudioSource为door_open

外侧门的控制代码使用上一任务的DoorController.cs即可
  Animator使用上一任务的NormalDoorController即可
    -- 复制一个AnimatorController,并将状态中的动画(Motion)换成对应的动画
       door_exit_outer_close和door_exit_outer_open

添加内侧门:prop_lift_exit

注意电梯门的方向 prop_lift_exit->door_exit_inner

在Lift上添加脚本LiftController.cs
  // 让inner door的z轴坐标跟随outer door的x轴坐标变化即可
  // 首先得到是个transform: inner_door和outer_door的
  innerDoorLeft.position = new Vector3(outerDoorLeft.position.x, 
    innerDoorLeft.position.y, innerDoorLeft.position.z);
        innerDoorRight.position = new Vector3(outerDoorRight.position.x, 
    innerDoorRight.position.y, innerDoorRight.position.z);

内侧门的开关实现了,但是速度会比较快,因此采用另一种方法 -- Lerp
  newInnerDoorLeftX = Mathf.Lerp(innerDoorLeft.position.x, 
    outerDoorLeft.position.x, Time.deltaTime);

再给内侧门左右加上BoxCollider

任务21:对电梯门添加钥匙控制

因为两种门共用一个脚本DoorController.cs
  使用一个bool keyRequired = false; 来判断该门是否需要钥匙?打开
  在PlayerController.cs中使用bool hasKey=false; 来记录当前是否拥有钥匙

  修改DoorController.cs中OnTriggerEnter部分:
    如果该门需要key,且collider为Player,且身上有key,才进行count++
    -- enemy在需要key的门前不会使其打开

    if(keyRequired) {
      if(other.tag == Tags.player) {
        if(other.gameObject.getcomponent<...script...>().GetHasKey()) {
          count++;
    }}} else { // normal door }

  对于OnTriggerExit() 也一样需要相同的判断,否则会平白无故count--;

  -- 小小不算bug的bug -- 在Trigger内部修改hasKey,就再也不能开门了
    逻辑,修改hasKey后出去, count--, count=-1, 再进去, count++, count=0;

将电梯外门的keyRequired勾选上即可

当没有钥匙的人接近了门的时候,需要播放AudioSource: door_accessDenied

  判断是否Player有钥匙后
  else {
    GetComponents<AudioSource>()[1].Play();  

任务22:主角拾起钥匙的功能

Models->prop_key_card

放置在这个位置
  

给Key添加Light,蓝色,intensity调大

创建Animator: KeyCardController
  将key_card中自带的默认动画spin拖入状态机即可 -- key的旋转动画

给Key添加Sphere Collider,用来作为Trigger,检测主角是否到达拾起钥匙的区域

给Key添加脚本KeycardController.cs
  OnTriggerEnter(...) {
    // Player进入触发区时,拾起钥匙
    PlayerController player = other.GetComponent<PlayerController>();
    player.hasKey = true;
    // 播放得到钥匙的声音
    -- 给Key添加一个AudioSource: keycard_pickUp
      通过audioSource.Play() 能行吗?不行,因为this已经被Destroy了
    解决方法:AudioClip -- 
      public AudioClip audio;
      // 在transform.position处播放audioClip
      AudioSource.PlayClipAtPoint(audio, transform.position);
    // 销毁钥匙
    Destroy(this.gameObject);
  }

任务23:激光的开关控制

游戏机制:一个电闸开关控制一个激光,Player在电闸旁边按下z来关闭对应激光

创建空物体switchUnits,用来存放所有(4个)开关

关闭开关后,激光会消失,开关上面锁的显示会改变(改变材质即可)

给SwitchUnit添加BoxCollider防止Player走进去
  添加BoxCollider作为Trigger -- Sphere不合适,在墙外也能触发
  添加AudioSource: switch_deactivation -- 解锁声音

添加脚本SwitchUnitController.cs控制开关逻辑

OnTriggerStay() { -- 需要在触发区域内
  if(other.tag == Tags.player) { -- 如果是Player进入
    if(Input.GetKeyDown(KeyCode.z)) { -- 如果按下了z键
      // 关闭对应激光 -- public GameObject laser;
      laser.SetActive(false);
      // 播放声音
      audio.Play();
      // 修改解锁标志 -- 替换material -- public Material unlockedMaterial;
      transform.Find("prop_switchUnit_screen").
        GetComponent<MeshRenderer>().material = unlockedMaterial;
}}}

-- 小Bug:在触发区域内多次按下z键,会进行多次的音效播放
  解决方法(个人):
    定义一个bool isSwitchValid = true
    在OnTriggerStay最外层用if(isSwitchValid) 判断即可

将其做成Prefab,添加其他四个SwitchUnit

private void OnTriggerStay(Collider other) {
    // 需要在触发区域内
    if (isSwitchValid) {
       if (other.tag == Tags.player) {
            // 如果是Player进入
            if (Input.GetKeyDown(KeyCode.Z)) {
                // 按下z键
                // 关闭对应激光 -- public GameObject laser;
                laser.SetActive(false);
                // 播放声音
                unlockedAudio.Play();
                // 修改解锁标志(替换material) - public Material unlockedMaterial;
                transform.Find("prop_switchUnit_screen").
                    GetComponent<MeshRenderer>().material = unlockedMaterial;
                isSwitchValid = false;
}}}}

任务24&25&26:摄像机的跟随移动 && 摄像机的视野问题 && 摄像机的缓动

按F聚焦Player,在Scene视图下调整好合适的视角
  选中MainCamera,GameObject->Align With View -- 将Camera设置为当前视角

摄像机的跟随移动
  会利用很多Lerp差值运算

在摄像机上添加脚本 FollowPlayer

// 得到相机和Player之间位置的offset
private Transform player = GameObject.FindWithTag(Tags.player).transform;
private Vector3 offset = tranform.position - player.position;
offset = new Vector3(0, offset.y, offset.z); // 保持x方向值相等

// 在Update中更新Camera位置
transform.position = player.position + offset;

出现问题:若主角在下侧墙的上方,则视野会被墙挡住
--改进相机的视野实现

摄像机的视野智能化调整:

解决思路:从Camera发射射线到Player,如果射线没有接触到Player,则旋转
  如何旋转?得到Player正上方的距离为offset的点,在该点和Camera之间等距划分
  三点,依次判断这三点是否符合要求,若符合,调整Camera位置和方向

代码实现:

// 起始点
Vector3 beginPos = player.position + offset;
// 终点 -- Player正上方点
Vector3 endPos = player.position + offset.magnitude * Vector3.up;

// 得到两点之间的四等分点 (3个点) -- 用向量运算方法或差值方法均可
Vector3 pos1 = Vector3.Lerp(beginPos, endPos, 0.25f);
其余的为pos2 = ... 0.5f;  pos3 = ... 0.75f;
存放在数组中:
Vector3[] posArray = new Vector3[5];

// 判断在哪个点可以看到Player -- 用射线的方法
foreach(Vector3 pos in posArray) {
  // 遍历所有可能位置
  RaycastHit hitInfo;
  if(Physics.Raycast(pos, player.position - pos, out hitInfo) {
    // 如果有碰撞的话
    if(hitInfo.collider.tag == Tags.player) {
      // 如果在视野内能看到Player -- 移动摄像机
      transform.position = pos;
      // 改变摄像机的朝向
      transform.LookAt(player.position);
      break;
}}}

现在,摄像机的视角问题解决了,但是摄像机需要进行缓动,不然会显得卡顿 -- Lerp 

摄像机的缓动:

将上面的transfrom.position = pos; 改为
transform.position = Vector3.Lerp(transform.position, pos, Time.deltaTime);

将上面的transform.LookAt(player); 改为
Quaternion currQuaternion = transform.rotation;
transform.LookAt(player);
transform.rotation = Quaternion.Slerp(currQuaternion, transform.rotation,
  Time.deltaTime);

private Transform player;
private Vector3 offset;
private float cameraMovingSpeed = 2;
private float cameraRotateSpeed = 10;

private void Start() {
    player = GameObject.FindWithTag(Tags.player).transform;
    // 保持在x轴方向值相同
    offset = transform.position - player.position;
}

private void Update() {
    // 得到起始点和终点
    Vector3 beginPos = player.position + offset;
    Vector3 endPos = player.position + offset.magnitude * Vector3.up;
    // 得到四等分点
    Vector3[] posArray = new Vector3[5];
    float lerp = 0.25f;
    float currLerp = 0;
    for (int i = 0; i < posArray.Length; i++) {
        posArray[i] = Vector3.Lerp(beginPos, endPos, currLerp);
        currLerp += lerp;
    }
    // 判断哪个点是适合的
    foreach (Vector3 pos in posArray) {
        RaycastHit hitInfo;
        if(Physics.Raycast(pos, player.position - pos, out hitInfo))
        Debug.DrawRay(pos, player.position - pos);
        print(hitInfo.collider);
        if(hitInfo.collider.tag == Tags.player) {
            // 在视野内能看到Player
            transform.position = Vector3.Lerp(transform.position, pos, 
                Time.deltaTime * cameraMovingSpeed);
            // 改变摄像机的朝向 -- 使用Quaternion的差值进行转向
            Quaternion currQuaternion = transform.rotation;
            transform.LookAt(player);
            transform.rotation = Quaternion.Slerp(currQuaternion, 
                transform.rotation, Time.deltaTime * cameraRotateSpeed);
            break;
}}}}

后期运行发现bug -- 当Player走到cctv_Cam下的collider的时候,也会导致camera视角变化
  将hitInfo.collider.tag == Tags.player修改为
  hitInfo.colllider.tag == Tags.player || hitInfo.collider.tag == Tags.cctvCollider

任务27:Navigation自动寻路导航网格

机器人的自动导航系统 -- 

把env_stealth_static和battle_bus设置为static (Navigation bake的目标)
Window->Navigation->Bake->Bake

检查一下自动生成的区域是否正确 -- 发现网布下方的通路没有打开


减小Agent Radius (增大区域的面积,离突起物更近的地方也可被视作可用区域)
减小Agent Height
Slope=0;因为在该场景下,是平面的,无需设置坡度
StepHeight=0;高度差小于这个值的,视为通路

任务28:添加机器人

Enemy: char_robotGuard -- AI实现、动画实现、射击实现等等

1. 防止碰撞墙体等
  添加碰撞器 Capsule Collider
  添加Rigidbody

2. 实现导航功能 -- 判断机器人当前应该去的方向
  添加Nav Mesh Agent组件
    Radius 自身宽度
    Speed 移动速度
    Acceleration 加速度
    AngularSpeed 旋转速度
    等等
  注:NavMeshAgent是可以控制运动的(直接设置position)
    但是由于动画也会控制运动,故不使用

3. 实现发现Player功能 -- 即机器人视野和听觉
  添加Sphere Collider作为Trigger
  当Player在Trigger之内,会进行机器人视野的检测

将机器人做成Prefab

任务29:设置机器人的状态机(动画效果)

创建Animator,名为EnemyController

BaseLayer -- Idle, Locomotion,
将动画humanoid_idle拖入状态机,set as default
创建BlendTree,名为Locomotion,表示机器人的运动
  机器人运动动画:
    Walk/Run, Walk/RunLeft/RightShort/Medium/Wide
    TurnOnSpotLeft/RightA/B/C/D
    将这些动画都添加如BlendTree的motion中
  参数为speed和rotation,因此为2d的BlendTree
    将BlendType改为2D Freeform Cartesian
  Compute Positions = Speed and AngularSpeed,数值会自动生成 


  可以看出来,每个点代表一个动画状态,
  PosX表示转向速度,PosY表示行走速度
  比如竖直中间两个点上面的为Run,下面的为Walk

需要两个Float Parameter变量分别对应AngularSpeed: PosX和Speed: PosY

发现Idle其实应该在Locomotion中,而不是单独存在
  删除Idle状态,将Idle动画添加到Locomotion中

射击动画:
  weapon_lower/weapon_raise/weapon_shoot
  增加一个Layer,称为Shoot
  添加一个bool Parameter: playerInSight,用来判断是否处于射击状态
  添加一个Empty状态作为默认状态 default
  transition: default -> weapon_raise : playerInSight = true
  transition: weapon_raise -> weapon_shooting : 没写condition时表示接着播放
    这里也可以写上playerInSight=true作为条件,表示raise后如果还是true才射击
    如果为false了就不进行射击了
  transition: weapon_raise -> weapon_lower : playerInSight = false;
  transition: weapon_shoot -> weapon_lower : playerInSight = false;
    如果playerInSight依然为true,则会进行循环播放
  transition: weapon_lower -> default : no condition

因为射击动画只控制了手的动作而没有控制身体
  因此需要创建一个身体遮罩
  在Project中create->Avatar Mask,命名为EnemyShootMask
  在Inspector中选择Humanoid,将全身禁止只留下双手和头部

点击状态机的Shoot Layer旁边的设置按钮,将EnemyShootMask赋值
  
  因为这一Layer权重比上一层高,所以Weight = 1,并选用Override

任务30:机器人的听觉和视觉

听觉:Sphere Collider
视觉:前方锥形范围 110°

添加脚本EnemySight.cs

private bool isPlayerInSight = false;
private float viewFieldAngle = 110;

视觉:

当主角在SphereCollider范围内 -- 在OnTriggerStay() 中
  if(other.tag = player) {
    // 得到两个方向:enemy前方和enemy朝向player的方向
    Vector3 forward = transform.forward;
    Vector3 playerDir = other.transform.position - transform.position;
    float tempAngle = Vector3.Angle(forward, playerDir);
    // 判断是否在锥形视野范围之内
    if(tempAngle <= viewFieldAngle * 0.5) {
      playerInSight = true;
    } else { playerInSight = false; }
  }

当主角不在SphereCollider范围内(不在听觉范围内) -- OnTriggerExit() 
  if(other.tag == player) {
    playerInSight = false; // 出了这个范围肯定是看不到了
  }

听觉:机制 -- 两点之间可通路的最短距离(Navigation),即墙等障碍物是完全隔音的
  听到声音后,会设置警报位置,让Enemy去追踪

存储当前警报位置 -- 被发现的Player位置
public Vector3 alertPosition = Vector3.zero; // 需要被外界使用,默认为zero
在上面Player被看到时,playerInSight=true;后加上 alertPos=other.transform.position;
  同理,playerInSight=false;后加上 alertPos=Vector3.zero; // 归零

在TriggerStay中(因为Trigger就是表示可以听到声音的范围)
// 判断player是否处于Locomotion状态
if(playerAnim.GetCurrentAnimatorStateInfo(0).IsName("Locomotion")) {
  // 使用NavMeshAgent判断最短路径距离
  // private NavMeshAgent navAgent = GetComponent<NavMeshAgent>();
  // 因为navAgent.CalculatePath(targetPos, navMeshPah), 需要一个空Path传入
    -- calculate a path to a specified point and stored the resulting path
       return a bool if there is a path
  NavMeshPath navPath = new NavMeshPath();
  // navPath.corners,为一个存放所有路径拐点的Vector3数组
  if(navAgent.CalculatePath(other.transform.position, navPath)) {
    Vector3[] waypoints = new Vector3[navPath.cornors.Length+2];

// 给所有路径点赋值
waypoints[0] = transform.position;
waypoints[waypoints.Length-1] = other.transform.position;
for(int i = 1; i < waypoints.Length - 1; i++) {
  waypoints[i] = navPath.corners[i-1];
}

// 计算总长度
float totalDistance = 0;
for(int i = 1; i < waypoints.Length; i++) {
  // 从第二个点开始,计算与前一个点之间的距离
  totalDistance += (waypoint[i-1] - waypoint[i]).magnitude;
}

// 总长度与最大长度进行比较,判断是否在最大距离之内
private SphereCollider sphereCollider = GetComponent<...>();
if(totalDistance <= sphereCollider.radius) { // 可以听到
  alertPosition = other.transform.position;

private void OnTriggerStay(Collider other) {
    if(other.tag == Tags.player) {
        // Player进入听觉视觉触发区域
        // 得到两个方向:enemy前方和enemy朝向player的方向
        Vector3 forward = transform.forward;
        Vector3 playerDir = other.transform.position - transform.position;
        float tempAngle = Vector3.Angle(forward, playerDir);
        // 判断是否在锥形视野范围之内
        if(tempAngle <= viewFieldAngle * 0.5f) {
            playerInSight = true;
            // 更新警报位置
            alertPosition = other.transform.position;
        } else {
            playerInSight = false;
            alertPosition = Vector3.zero;
        }

        // 听觉实现 -- 通路距离在给定范围之内,则进行追踪
        if (playerAnim.GetCurrentAnimatorStateInfo(0).IsName("Locomotion")) {
            // 若当前Player状态为Locomotion -- 有声音发出
            NavMeshPath navPath = new NavMeshPath();
            if (navAgent.CalculatePath(other.transform.position, navPath)) {
                Vector3[] waypoints = new Vector3[navPath.corners.Length + 2];
                waypoints[0] = transform.position;
                waypoints[waypoints.Length - 1] = other.transform.position;
                for (int i = 1; i < waypoints.Length - 1; i++) {
                    waypoints[i] = navPath.corners[i - 1];
                }
                // 计算路径总长度
                float totalDistance = 0;
                for (int i = 1; i < waypoints.Length; i++) {
                    totalDistance += (waypoints[i-1]-waypoints[i]).magnitude;
                }
                // 判断是否在最大距离内 -- 最大距离即为SphereCollider的半径
                if (totalDistance <= sphereCollider.radius) {
                    alertPosition = other.transform.position;
}}}}}

private void OnTriggerExit(Collider other) {
    if(other.tag == Tags.player) {
        playerInSight = false;
        alertPosition = Vector3.zero;
}}

运行,听觉和视觉都正常运行
  Bug1: 但是,因为SphereCollider的关系,Camera.FollowPlayer()中需要加上另一个条件
    hitInfo.collider.tag == Enemy

  Bug2: 机器人移动到门附近的时候,因为SphereCollider范围很大,会触发门的开关
    而只有当CapsuleCollider进入的时候才会触发门的开关
    改为:if(tag... && other.GetType().ToString()=="UnityEngine.CapsuleColiider")
      或: if (other.tag == Tags.enemy && other is CapsuleCollider) {
      或: if (other.tag == Tags.enemy && other.isTrigger == false) {
    即可

任务31:机器人发起警报的功能

警报机制:当Enemy看到Player时,通知其他机器人进行追踪
  看到或听到都会更新警报发出位置

记得之前在GameController中有一个警报方法SwitchAlarmOn将isAlarmOn设置为true

那么,在EnemySight.cs中,
  当isPlayerInSight为true时,加上一句:
  GameController._instance.SwitchAlarmOn(other.transform); // 将player位置传递

实现了Enmey看到Player时,开启警报
还需实现追踪功能
  机制:alertPos不等于Vector3.zero时,就进行追踪
     而且,GameController中的lastPlayerPos修改时 (其他警报器),也要进行追踪

当lastPlayerPos修改时,更新
  // (Awake()中)用来存储之前的lastPlayerPos;
  private Vector3 preLastPlayerPos = GameController._instance.lastPlayerPos;
  // 进行检测 -- Update()中
  if(preLastPlayerPos != GameController._instance.lastPlayerPos) {
    alertPosition = GameController._instance.lastPlayerPos;
    preLastPlayerPos = alertPosition;
  }

报错: preLastPlayerPosition = GameController._instance.lastPlayerPos;
  
原因:GameController.cs中_instance = this的赋值在Awake()中
     而preLastPlayerPos这句话的赋值也是在Awake()中且用到了_instance
     因此可能会出现空指针的错误

解决方法:将preLastPlayerPos的赋值放在Start()中

现在可以通过cctvCam和laser触发警报并修改alertPosition的值,
Bug -- 在cctvCam中移动不会更新lastPlayerPos:
  cctvCamCollision.cs中的触发条件改为OnTriggerStay() 而不是OnTriggerEnter()

Bug -- 在SphereCollider之内且playerInSight=false时,alertPosition会被强制设为zero
  因为在两个else情况下,把alertPosition设置为了Vector3.zero
  解决方法:去掉OnTriggerStay中的设置即可,OnTriggerExit需要设置为zero

任务32&33:使用Navigation控制机器人的巡逻 && 行走的动画

创建空物体waypoints,用来存储巡逻的路径点
  在里面创建若干个空物体waypoint -- 用来表示路径的下一个临时目标位置
  分别在Scene中设置position
    但是空物体在环境中是没有图标表示的,因此不好观察
    在Inspector中物体名的左边可以选择icon->other->waypoint

注意:使用NavMeshAgent进行控制,是直接设置transform的

创建脚本EnemyMoveAI.cs

// 定义数组,存储waypoints的位置
public Transform[] waypoints = new Transform[4];
private int index = 0; // 当前目标索引

巡逻的方法:
private void Patrolling() {
  -- 机制:巡逻到达一个waypoint后,需要休息一段时间,再去下一个waypoint
  -- private float patrolTimer = 0;
  -- private float patrolWaitingTime = 3;
  // 需要使用NavMeshAgent进行路径导航
  -- private NavMeshAgent navAgent = GetComponent<...>();
  -- Awake()中,设navAgent.destination = waypoints[index].position; //index=0
  // 检测有没到达目标位置
  if(navAgent.remainingDistance < 0.5f) {
    // 休息
    navAgent.isStoped = true;
    patrolTimer += Time.deltaTime;
    if(patrolTimer >= patrolWaitingTime) {
      // 休息完毕,下一个点
      // 一共四个点,循环播放,index不能超过三
      index++;
      index %= 4;
      navAgent.destination = waypoints[index].position;
      navAgent.isStoped = false; // 开始进行下一个点的寻路
      patrolTimer = 0;
  }}

现在,机器人可以进行四个点的巡逻的,但是,没有行走动画的播放,仅仅是位置的移动
  NavMeshAgent提供的寻路为直接修改transform的,我们利用它的指向功能
  结合动画,来实现动画版的巡逻

将NavMeshAgent的位置控制取消
  navAgent.updatePosition = false;
  navAgent.updateRotation = false; 

新建脚本EnemyAnimationController.cs -- 通过控制状态机来控制行动

private NavMeshAgent navAgent = GetComponent<...>();

在Update()里
navMesh.desiredVelocity // 期望速度 -- 当前移动的速度和方向
思路:navMesh的期望速度设置给用来控制动画状态机的Speed和AngularSpeed

当休息时(即navAgent.isStopped = true)
if(navAgent.desiredVelocity = Vector3.zero) {
  enemyAnim.SetFloat("speed", 0);
  enemyAnim.SetFloat("angularSpeed", 0);
  // 注意 重载 SetFloat(name, value, dampTime, deltaTime)
    dampTime: 阻尼时间,在该时间内,将name逐渐设置为value,渐变过程
  // 直接设置,动画有时候会显得突兀,这里先不用,看看效果不好再用
} else {  -- 行走状态

  // 计算角度AngularSpeed
  float angle = Vector3.Angle(transform.forward, navAgent.desiredVelocity);
  // anim中的AngularSpeed为弧度值
  float angleRad = Mathf.Deg2Rad * angle;  // Dot = 2PI/360
  // 算出弧度的方向(正负) -- 根据动画来说,右转为正
  -- Vector3.Cross(a,b)
    方向:应用左手定则,result向量(中指)分别垂直于a(拇指)和b(食指)
    大小:为a和b大小的乘积
  思路:通过Cross的方向即可得angleRad的正负:向上为正,向下为负
  Vector crossResult = Vector3.Cross(...forward, ...desiredVelocity);
  if(crossResult.y < 0) { angleRad = -angleRad; }
  anim.SetFloat("angularSpeed", angularSpeed);

  思路:当角度大于九十度时,没必要往前走了,便设定为原地旋转,
     当角度小于九十度时,边走边旋转
  if(angle >= 90) {
    anim.SetFloat("speed", 0);
  } else {
    // 计算当前速度
    
    // ab为当前朝向,ac为desiredVelocity
    // 当前速度设为ac在ab方向上的分速度ad即可
    慢慢加快速度,动画会平滑很多
    -- Vector3.Project(vector, onNormal) :
       返回vector在onNormal方向上的分向量projection
    Vector3 projection = Vector3.Project(...desiredVelocity, ...forward);
    anim.SetFloat("speed", projection.magnitude);
  }

Bug -- NavMeshAgent的位置与机器人的位置分离了
  Siki上没有出现这个情况
  同样的问题:http://tieba.baidu.com/p/5313022190?traceid=
  解决方法:在Update() 最后将NavMeshAgent的位置设置为transform的位置即可
    navAgent.nextPosition = transform.position;

public class EnemyAnimationController : MonoBehaviour {
    public float speedDampTime = 0.3f;
    public float angularSpeedDampTime = 0.3f;
    private Animator enemyAnim;
    private NavMeshAgent navAgent;

    void Awake () {
            enemyAnim = GetComponent<Animator>();
            navAgent = GetComponent<NavMeshAgent>();
    }
    
    void Update () {
        // 如果navAgent.isStopped = true -- 休息状态
        if(navAgent.desiredVelocity == Vector3.zero) {
            enemyAnim.SetFloat("speed", 0, speedDampTime, Time.deltaTime);
            enemyAnim.SetFloat("angularSpeed", 0, 
                angularSpeedDampTime, Time.deltaTime);
        } else {
            // 行走状态
            // 旋转角度大小
            float angle = Vector3.Angle(transform.forward,         
                navAgent.desiredVelocity);
            float angleRad = angle * Mathf.Deg2Rad;
            // 旋转角度方向
            Vector3 crossResult = Vector3.Cross(transform.forward, 
                navAgent.desiredVelocity);
            if(crossResult.y < 0) { // 左转
                angleRad = -angleRad;
            }
            enemyAnim.SetFloat("angularSpeed", angleRad, 
                angularSpeedDampTime, Time.deltaTime);
            // 两种情况,角度大于90时,原地旋转;小于90时,边走便旋转
            if(angle >= 90) {
                // 原地旋转
                enemyAnim.SetFloat("speed", 0, speedDampTime, Time.deltaTime);
            } else {
                // 计算speed -- desiredVelocity在当前方向forward上的分速度
                Vector3 projection = Vector3.Project(navAgent.desiredVelocity, 
                    transform.forward);
                enemyAnim.SetFloat("speed", projection.magnitude, 
                    speedDampTime, Time.deltaTime);
            }
        }
        navAgent.nextPosition = transform.position;
    }
}

任务34:机器人的追捕 AI

机器人的三种状态:巡逻、追捕、射击
  巡逻:普通
  追捕:alertPosition != Vector3.zero
  射击:EnemySight.isPlayerInSight == true;

EnemyMoveAI.cs中

追捕方法:

private void Chasing() {
  nagAgent.speed = 5; // 追捕时速度快 (记住在Patrolling()中navAgent.speed=3;)
  navAgent.destination = enemySight.alertPosition; (EnemySight sight = ...)
  // 追捕时,目标距离大一些,就可进行射击()
  if(navAgent.remainingDistance < 2f) {
    到达目标位置后,若看见Player就自动射击(在Animator和Shooting()中控制)
    如果没看见,alertPosition也没更新,就在原地停留一段时间,解除警报
    -- public chaseWaitingTime = 5;
    -- private chaseTimer = 0;
    chaseTimer += Time.deltaTime;
    if(chaseTimer > chaseWaitingTime) {
      // 解除警报,回到巡逻位置
      sight.alertPosition = Vector3.zero;
      GameController._instance.lastPlayerPosition = Vector3.zero;
      GameController._instance.isAlarmOn = false;
      index = 0;
      navAgent.destination = waypoints[index];
      chaseTimer = 0; //计时器归零
    }

任务35:解决bug:视野检测和自动门的开启和关闭

Bug -- 视野检测:若Player与Enemey之间有障碍物(墙),Enemy也是可见Player的
  解决方法:若在视野viewFieldAngle内,则在Enemy眼睛处发射一条射线,方向为playerDir

Raycast的起点为transform.position + Vector3.up * 1.8f; 终点为alertPos
RaycastHit hitInfo;
Physics.Raycast(transform.position+Vector3.up*1.8f, playerDir, out hitInfo);
if(hitInfo.collider.tag == Tags.player) {
  在视野内
}

if (tempAngle <= viewFieldAngle * 0.5f) {
    // 判断是否有障碍物(如墙)挡住视线
    RaycastHit hitInfo;
    Physics.Raycast(transform.position + Vector3.up * 1.8f, playerDir, out hitInfo);
    if(hitInfo.collider.tag == Tags.player) {
        playerInSight = true;
        // 更新警报位置
        alertPosition = other.transform.position;
        GameController._instance.SwitchAlarmOn(other.transform);
    } else {
        playerInSight = false;
    }
} else {
    playerInSight = false;
}

Bug -- 门的控制在Player和Enemy同时进出的时候有问题
  解决方法:将player和enemy分别判断并各自count++/--即可

任务36&37:机器人的射击动画 && 伤害计算

 

射击动画:

void Update () {
    if(enemySight.playerInSight) {
        // 只要能看见Player,就射击
        Shoot();
    } else if (enemySight.alertPosition != Vector3.zero) {
        // 有动静 -- 进行追捕
        Chasing();
    } else {
        Patrolling();
    }
}

在Shoot()中不处理动画
  navAgent.isStopped = true; // 停止导航

-- 调用EnemyAnimationController.cs来控制射击动画的播放
在Update()控制行走的代码后,通过playerInSight的赋值来控制动画
  private EnemySight enemySight = GetComponent...
  enemyAnim.SetBool("playerInSight", enemySight.playerInSight);

伤害计算:

在player上添加脚本PlayerHealth.cs

public float hp = 100; // 血量

private void TakeDamege(float damage) {
  hp -= damage;
}

public bool isPlayerAlive() {
  return (hp>0);

在EnemyMoveAI.cs中
  在Update()判断Shoot()或Chasing()时,加上判断如果isPlayerAlive才进行该操作

检测enemy状态机中humanoid_weapon_shoot动画的播放:播放一次进行一次伤害计算
  动画->Inspector->Curves->Shot:看曲线的变化
  在状态机中设置一个参数,用来记录上面Shot曲线的当前值
    添加Parameter float Shot, 这个值会自动被曲线Shot的当前值赋值

在Enemy上添加脚本EnemyShooting() -- 用来处理射击有关的操作

在Update()中
if(anim.GetFloat("Shot") > 0.5f) { // 进行了一次射击
  Shooting();
} else {
  hasShot = false; // 没有开枪,则标志位重置
}

伤害机制:距离越近伤害越大,伤害有保底值
  主角在SphereCollider范围之内会进行射击,则在最外侧伤害最低
public float minDamage = 30;

private void Shooting() {  // 计算伤害
  -- private bool hasShot = false;
  if(!hasShot) { // 进行伤害
    -- 从PlayerHealth脚本获得
    // 计算伤害
    float distance = Vector3.Distance(transform.position, playerHealth.transform.pos);
    float damage = minDamage+ (100-minDamage)*(distance/sphereCollider.radius);
    playerHealth.TakeDamage(damage);
    hasShot = true;
  }

Player死亡动画:

回顾:在Player的动画状态机中的Parameter bool dead;
  dead = true时 播放dead动画

在PlayerHealth.cs中持有animator
  在TakeDamage() 中判断
    if(!IsPlayerAlive()) {
      anim.SetBool("dead", true);
    }

Bug -- Player死亡以后,enemy还是会在盯着,没有回去巡逻
  解决方法:在EnemySight中触发playerInSight的判断条件里加上
    && playerHealth.isPlayerAlive()

  而且Shooting()时把navAgent设为Stopped了
        Enemy从Shooting转为Patrolling时候需要重新设置开始
    navAgent.isStopped = false;
    // navAgent.destination =  waypoints[index].position; // 不要也行

Bug -- Enemy在PlayerInSight=true的状态下正在射击,但Player跑出了射击范围
  此时Enemy就会呆在原地不动
  思路:与上面一个bug相似,Shooting后navAgent被stop了,下一帧转为Chasing()时还是true
    在Chasing()刚开始的时候,navAgent.isStopped = false;
    

Bug -- Player死亡以后,死亡动画会不断播放
  思路:猜想是因为状态机中Any State--dead=true-->Dead
    AnyState也包括了Dead本身,于是有Dead->Dead的动画
      https://blog.csdn.net/ln_polaris/article/details/50724425
    将transition属性取消勾选Can Transition To Self即可

任务38&39:添加其他机器人 和 游戏失败状态 && 游戏胜利状态

将robot apply给prefab

创建空物体Robots,用来存储其他两个敌人(一共三个)

创建对应的waypoints

游戏失败状态:
  机制:死亡之后,等待4秒,重新加载Scene

IEnumerator ReloadScene() {
  yield return new WaitForSeconds(4f);
  SceneManager.LoadScene(0);
}

在TakeDamage()中
  在播放死亡动画之后
  StartCoroutine(ReloadScene());

播放失败音效
  添加AudioSource组件: endgame
  defeatAudio.Play();

游戏胜利状态:
  机制:胜利之后,启动电梯,并重新加载Scene

prop_lift_exit

添加BoxCollider作为Trigger

编辑之前创建的脚本LiftController.cs

private float liftUpWaitingTime = 3;
private float liftUpTimer = 0;
private bool isPlayerInside = false;

分别在OnTriggerEnter/Exit()中控制isPlayerInside,记得判断collider.tag为player

在Update()中:
  if(isPlayerInside) {  // player进入电梯
    liftUpTimer += Time.deltaTime;
    if(liftUpTimer > liftUpWaitingTime) { // 等待时间到,升起电梯
      -- 电梯只会升起一次
      // 播放电梯升起音效
      liftUpAudio.Play();
    }

发现,若干秒后电梯向上移动了,但是:

1. Player没有随着电梯上升而上升
  原因:电梯地板没有设置collider
    给电梯添加Mesh Collider, 选择mesh: prop_lift_exit)collision_001
  原因:player的position.y在Inspector中Freeze,取消勾选

2. 电梯上升了一小段便停下了
  原因未知,但是修改完bug1后这个bug自动消失了

电梯上升后,游戏胜利
  机制:电梯上升后一段时间,宣布游戏胜利,重新载入Scene

private float gameWinTimer = 0;
private float gameWinWaitingTime = 4;

在电梯上升的代码块中:
gameWinTimer += Time.deltaTime;
if (gameWinTimer > gameWinTime) {
  SceneManager.LoadScene(0);
}

public class LiftController : MonoBehaviour {
    ...
    private float liftUpWaitingTime = 3;
    private float liftUpTimer = 0;
    private float gameWinWaitingTime = 5;
    private float gameWinTimer = 0;
    private bool isPlayerInside = false;

    void Update () {
        ...
        // 升起电梯
        if(isPlayerInside) {
            // Player在电梯内
            liftUpTimer += Time.deltaTime;
            if(liftUpTimer > liftUpWaitingTime) {
                // 等待时间结束
                transform.Translate(Vector3.up * Time.deltaTime);
                gameWinTimer += Time.deltaTime;
                if(gameWinTimer > gameWinWaitingTime) {
                    SceneManager.LoadScene(0);
    }}}}

    private void OnTriggerEnter (Collider other) {
        if(other.tag == Tags.player){
            isPlayerInside = true;
        }}

    private void OnTriggerExit (Collider other) {
        if(other.tag == Tags.player) {
            isPlayerInside = false;         
}}}
    

任务40:游戏提示设置 和 总结

创建UI->Text,字体为Sansation_Light,位置设置为左下角,颜色灰白色

创建脚本MessageShow.cs

void Start () {
    msg = GetComponent<Text>();
    msg.text = "WASD to Move\n" +
        "Z to Switch Off\n" +
        "LeftShift to Sneak";
}

 

 

 

转载于:https://www.cnblogs.com/FudgeBear/p/8717949.html

2015-04-29 14:41:42 nijiayy 阅读数 1978
  • Unity 值得看的500+ 技术内容列表

    Unity3D是由Unity Technologies开发的一个让玩家轻松创建诸如三维视频游戏、建筑可视化、实时三维动画等类型互动内容的多平台的综合型游戏开发工具,是一个全面整合的专业游戏引擎。

好的,下面我们来说一下,小车的创建,怎么实现随机的创建小车,其实在创建小车这里我主要是用来Instantiate函数来克隆prefab物体,当然这个预制物体也是需要设置一番的,看了视频的话我们肯定都已经知道了wheelcollider的创建,其实一个预制物体还有一个引导球包括他的脚本和一个包裹小车的用于感知其他小球碰撞的“保护膜”,

这里有一个层次结构大家可以看一下:


当前选中的那个其实是小车的外壳,他的作用就是在周围包裹一层collider来保证我们的小车不会从我们的场景中掉下去,用于控制我们小车的脚本是在那个父物体Pickup_Truck中,这样在一个预制物体生成的时候就会有相应的函数来执行,下面来看一个放在小车上的另一个脚本也就是控制小车的脚本:

#pragma strict
    var collider_FL:WheelCollider;
	var collider_FR:WheelCollider;
	var collider_BL:WheelCollider;
	var collider_BR:WheelCollider;

	var wheel_FL:Transform;
	var wheel_FR:Transform;
	var wheel_BL:Transform;
	var wheel_BR:Transform;

	private var left_light:GameObject;
	private var right_light:GameObject;
	private var bot_light:GameObject;
	private var top_light:GameObject;

    var target:Transform;	
	private var body:Rigidbody;
	private var motor:float;
	var CenterOfMass:Vector3;
	private var maxangle:float=40f;
	private var brake:float;//右转刹车
	private var go:int ;//是否继续前行的标志
	private var car_front:int ;//if any car in front 
	// Use this for initialization
function Start () {
		go=1;
		brake=0;
		motor=20;
		body=GetComponent("Rigidbody");
		body.centerOfMass=CenterOfMass;//用于重心的控制
		//left_light=GameObject.Find("traffic_lights_left");
		right_light=GameObject.Find("traffic_lights_right");
		bot_light=GameObject.Find("traffic_lights_bot");
		top_light=GameObject.Find("traffic_lights_top");
}

function Update () {
	   //渲染车轮的运动
		wheel_FL.Rotate(collider_FL.rpm/60*360*Time.deltaTime,0,0);
		wheel_FR.Rotate(collider_FR.rpm/60*360*Time.deltaTime,0,0);
		wheel_BL.Rotate(collider_BL.rpm/60*360*Time.deltaTime,0,0);
		wheel_BR.Rotate(collider_BR.rpm/60*360*Time.deltaTime,0,0);
		//渲染前轮的旋转
		wheel_FL.localEulerAngles=wheel_FR.localEulerAngles=new Vector3(wheel_FL.localEulerAngles.x,collider_FL.steerAngle,wheel_FR.localEulerAngles.z);
}

function FixedUpdate(){//表示物理操作的更新
	   //用于更新当前前轮的状态
		var offsetTargetPos:Vector3= target.position;//小球的位置
		var localTarget:Vector3= transform.InverseTransformPoint(offsetTargetPos);
		var targetAngle:float = Mathf.Atan2(localTarget.x, localTarget.z)*Mathf.Rad2Deg;
		var steer:float = Mathf.Clamp(targetAngle*0.05f, -1, 1)*Mathf.Sign(GetComponent.<Rigidbody>().velocity.magnitude);
		collider_FL.steerAngle=collider_FR.steerAngle=steer*maxangle;
		if(go==1&&car_front==0){//go判断前方是否有车是否是红灯
		  
		  brake=0;
		  motor=20;
		}else{//stop
		  motor=0;//动力
		  brake=25000;//刹车
		  var newx:float=body.velocity.x-1>0?body.velocity.x-1:0;
		  body.velocity=new Vector3(newx,0,0);
		  isgoon();//用于红灯时不断地检测当前信号灯的状态
		}
		collider_BL.motorTorque=collider_BR.motorTorque=motor;
		collider_BL.brakeTorque=collider_BR.brakeTorque=brake;
		
	}
function isbrake(a:int){
		brake=a;
		if(a==0){
		motor=20;
		}else{
		motor=0;
		}
		//Debug.Log (transform.name+" brake is "+brake);
	}
function isgo(a:int){
      car_front=a;
      //Debug.Log(transform.name+" car_front:"+car_front);
	}	
function isgoon(){
        var way_name=getway();
		switch(way_name){
		case "left":
			go=lightcontrol.go;
			break;
		case "right":
		    go=lightcontrol.go;
			break;
		case "bottom":
		    go=tblightcontrol.go;
			break;
		case "top":
		    go=tblightcontrol.go;
			break;
		case "notinroad"://包括最右面用于右转的那个车道
		    go=1;
		    break;
		}
	}
function OnTriggerEnter(go:Collider ) {
        if(go.transform.name=="trigger"){
         gameObject.BroadcastMessage("lastcollider",go.transform.name);
         }
		if(go.transform.name=="stop_line"){//也就是在路口前
			isgoon();
		//    Debug.Log("stop");
		}
	}
	
	//用于判断当前在哪条道路上
function getway(){
		var x:float=transform.position.x;
		var z:float=transform.position.z;
		//这里的车道的计算是不包括最右车道的,因为最右车道不是右拐就是紧急车道没有红灯
		if(x>0&&x<89&&z>96&&z<105.5){//left
			return "left";
		}else if(x>107&&x<200&&z>90.5&&z<102.5){//right
			return "right";
		}else if(x>89&&x<101&&z>0&&z<90.5){//bottom
			return "bottom";
		}else if(x>94&&x<107&&z>105.5&&z<200){//top
			return "top";
		}else{
			return "notinroad";
		}
	}

这里的注释也是比较详细,这里要说一下的就是那个控制前车轮的变化角度的计算,这个是我借鉴unity的car_ai的例子写的,主要是计算了一个正切角什么的,具体的时我也没太弄懂...不过可以使用,我就直接拿下来了(看来学好数学还是必须的!)。那个判断当前位置的函数的具体的x,z(y 是垂直的轴)是要大家自己计算的,不同的物理世界肯定是不同,这里只是给出了一个用于判断当前位置的方法~



2015-12-24 00:06:19 ybhjx 阅读数 595
  • Unity 值得看的500+ 技术内容列表

    Unity3D是由Unity Technologies开发的一个让玩家轻松创建诸如三维视频游戏、建筑可视化、实时三维动画等类型互动内容的多平台的综合型游戏开发工具,是一个全面整合的专业游戏引擎。

控制AI战车前轮左右转弯的脚本

  //----------------

  var EnemyCar: GameObject; //定义敌人

  var attackRange = 100.0; //定义距离

  var target : Transform;//定义目标为自己

  EnemyCar=GameObject.Find(“Enemy”); //实例化

  if (target == null)

  return;

  var targetPoint = target.position;

  var targetRotation = Quaternion.LookRotation (targetPoint - EnemyCar.transform.position,Vector3.up); //求出与目标之间的角度 可以判断角度

  EnemyCar.transform.rotation = Quaternion.Slerp(EnemyCar.transform.rotation, targetRotation, Time.deltaTime * 2.0); //旋转之角度与目标对齐 可以换成车轮旋转