|
文 / 0!1!
现在手游火的一塌糊涂,引擎也是层出不穷除了引领3D市场的Unity3D,独霸2D市场的Cocos2D-X之外,还有虚幻、Sphinx等,甚至搜狐也开发了国产的Genesis-3D引擎。
文章适合人群:对Unity基础组件有一些了解的,想知道怎么在项目中具体应用各种组件。
这篇文章以一个Asset Store上面的例子“Unity Projects Stealth”来讲解Unity的一些知识。所以可能你要对Unity一些概念有个了解。
在此之前先说下本文运行项目时的环境:
系统 :Window 7 X64
Unity:4.3.3f1
1、项目大致了解
我们打开unity,新建项目,然后导入上面下载的"Unity Projects Stealth.unitypackage"。等导入完毕后就可以看到“Project”视图(如果你没打开,可以从菜单栏“Window”-“Project”打开)里面的结构如下:
里面每个目录大家都可以随便打开看看:
Animations | 一些角色动画 | Animator | 动画控制器,实际里面啥都没有-_- | Audio | 音频文件 | Fonts | 字体 | Gizmos | 在scene视图用于调试的一些图标 | Materials | 材质 | Models | 模型 | Prefabs | 预设 | Scenes | 场景文件,实际里面啥都没有-_- | Scripts | 脚本,实际里面啥都没有-_- | Shaders | 着色器 | Textures | 一些贴图纹理 | Done | 这里面才包含了上面缺少的动画控制器、场景文件和脚本
|
大家平常做项目也可以参考上面的做法,对不同的资源放不同目录进行归类整理,而且大家一看到这些文件夹就都知道里面有什么资源。可以减少很多沟通成本,也为自己查找资源带来便利。
好了,如果没什么问题,点击播放按钮,应该就能运行项目了。
使用键盘的WASD或上下左右箭头就可以控制人物走动了。屏幕左下方也有文本给出其它操作的按键。比如"Z"可以打开开关等。
2、碰撞器基础
一般情况下,你想让物体直接有体积,能产生碰撞,那么你需要给这个物体增加一个碰撞器“Collider”。在Unity中,你只需要选择一个对象,然后点击菜单栏的”Component“-“Physics”,里面就有各种各样的碰撞器,你根据自己模型选择一个比较接近的就OK了。Unity里面的碰撞器区分为两种,一种为静态的,另一种动态的。
静态的意思是这个碰撞器在游戏过程中不会发生位移、旋转、缩放。
动态的意思则是说这个碰撞器可能会在游戏过程中发生位移、旋转、缩放。动态碰撞器有两种:
一种是CharacterController。
另一种是普通的碰撞器+刚体组件。
第二种一定要增加刚体组件。不然可能会导致碰撞失效、性能开销增加。(比如Unity一个UI组件NGUI,它新版本的Panel会检测当前对象是否带了Rigibody,如果没有则自动增加一个。就是为了防止开发者做一些界面动画,忘记修改添加Rigibody,导致UI按钮点击失效。)
3、控制角色
控制一个角色在场景中运动,最简单的做法是把一个对象拖到场景中,然后根据按键。设置这个对象transform的position值。这样对象就可以在场景中运动起来。
获取按键可以用Input.GetAxis方法。获取X和Z轴的按键分别可以使用:Input.GetAxis("Horizontal") 和 Input.GetAxis("Vertical")方法。“Horizontal”和“Vertical”其实是在“Edit”-“Project Settings”-“Input”里面配置的。
那么我们这个例子是使用什么方法来让玩家角色运动的呢?
先在“Hierarchy”视图(如果没有这个视图,可以在“Window”-“Hierarchy”打开它)中找到“char_ethan”对象,选中它。
我们先看看它的“Inspector”视图(如果没有可以在“Window”-“Inspector”打开它)。
注意这里除了有基本的Transform组件外,还有“Animator”,“Capsule Collider”,“Rigidbody”组件。这三个组件其实用于做可移动物体其实是黄金组合(当然也有用“Character Controller”的)。
还有“Audio Source”、“Audio Listener”两个分别是播放声音和监听声音的组件。
然后"Done Player Health (Script)","Done Player Inventory (Script)","Done Player Movement (Script)"分别是对角色做控制的脚本。下面我们简单说说每个组件的用途:
A、Animator
其实是Unity内置的一个动画控制器,原理是状态机。比如双击上面的“Animator”面板里面的“Controller”属性:
就会出现状态机的编辑窗口:
B、Capsule Collider
胶囊体碰撞器。比较简单,没什么好说的。
C、Rigidbody
刚刚上面也说了,动态碰撞器的需要。
关于A、B、C这几个组件上面就只简单介绍到这里,这里不展开讨论。如果有需要,我后面会再单独写文章介绍。
下面开始看看控制代码部分。
D、DonePlayerMovement
这里有个“DonePlayerMovement(Script)”组件,从名字上看就知道应该是控制移动的(所以说命名很重要)。我们打开这个组件的源码,看看。
- using UnityEngine;
- using System.Collections;
-
- public class DonePlayerMovement : MonoBehaviour
- {
- public AudioClip shoutingClip; // 玩家大喊的声音
- public float turnSmoothing = 15f; // 用于玩家平滑转向的值
- public float speedDampTime = 0.1f; // 用于控制从一个值变化到另一个的时间限制
-
-
- private Animator anim;
- private DoneHashIDs hash; // 保存各种动画状态的hash
-
-
- void Awake ()
- {
- anim = GetComponent<Animator>();
- hash = GameObject.FindGameObjectWithTag(DoneTags.gameController).GetComponent<DoneHashIDs>();
-
- // Set the weight of the shouting layer to 1.
- anim.SetLayerWeight(1, 1f);
- }
-
-
- void FixedUpdate ()
- {
- // Cache the inputs.
- float h = Input.GetAxis("Horizontal");
- float v = Input.GetAxis("Vertical");
- bool sneak = Input.GetButton("Sneak");
-
- MovementManagement(h, v, sneak);
- }
-
-
- void Update ()
- {
- // Cache the attention attracting input.
- bool shout = Input.GetButtonDown("Attract");
-
- // Set the animator shouting parameter.
- anim.SetBool(hash.shoutingBool, shout);
-
- AudioManagement(shout);
- }
-
-
- void MovementManagement (float horizontal, float vertical, bool sneaking)
- {
- // Set the sneaking parameter to the sneak input.
- anim.SetBool(hash.sneakingBool, sneaking);
-
- // If there is some axis input...
- if(horizontal != 0f || vertical != 0f)
- {
- // ... set the players rotation and set the speed parameter to 5.5f.
- Rotating(horizontal, vertical);
- anim.SetFloat(hash.speedFloat, 5.5f, speedDampTime, Time.deltaTime);
- }
- else
- // Otherwise set the speed parameter to 0.
- anim.SetFloat(hash.speedFloat, 0);
- }
-
-
- void Rotating (float horizontal, float vertical)
- {
- // Create a new vector of the horizontal and vertical inputs.
- Vector3 targetDirection = new Vector3(horizontal, 0f, vertical);
-
- // Create a rotation based on this new vector assuming that up is the global y axis.
- Quaternion targetRotation = Quaternion.LookRotation(targetDirection, Vector3.up);
-
- // Create a rotation that is an increment closer to the target rotation from the player's rotation.
- Quaternion newRotation = Quaternion.Lerp(rigidbody.rotation, targetRotation, turnSmoothing * Time.deltaTime);
-
- // Change the players rotation to this new rotation.
- rigidbody.MoveRotation(newRotation);
- }
-
-
- void AudioManagement (bool shout)
- {
- // If the player is currently in the run state...
- if(anim.GetCurrentAnimatorStateInfo(0).nameHash == hash.locomotionState)
- {
- // ... and if the footsteps are not playing...
- if(!audio.isPlaying)
- // ... play them.
- audio.Play();
- }
- else
- // Otherwise stop the footsteps.
- audio.Stop();
-
- // If the shout input has been pressed...
- if(shout)
- // ... play the shouting clip where we are.
- AudioSource.PlayClipAtPoint(shoutingClip, transform.position);
- }
- }
复制代码
这个脚本整体来说:
在Awake的时候,先找到脚本需要用到的Animator组件和DoneHashIds组件,然后缓存它们(在后面用到就不需要频繁查找了,节省CPU)。
在FixedUpdate的时候,获取玩家按下的移动键,并处理移动、角色朝向问题。
在Update的时候,获取玩家是否按下“Attract”键,并确定是否播放动画以及声音。
OK,我们主要来看看MovementManagement这个方法:
- void MovementManagement (float horizontal, float vertical, bool sneaking)
- {
- // Set the sneaking parameter to the sneak input.
- anim.SetBool(hash.sneakingBool, sneaking);
-
- // If there is some axis input...
- if(horizontal != 0f || vertical != 0f)
- {
- // ... set the players rotation and set the speed parameter to 5.5f.
- Rotating(horizontal, vertical);
- anim.SetFloat(hash.speedFloat, 5.5f, speedDampTime, Time.deltaTime);
- }
- else
- // Otherwise set the speed parameter to 0.
- anim.SetFloat(hash.speedFloat, 0);
- }
复制代码
我们可以发现,除了Rotating方法,基本都是对anim做一些状态设置的方法。那么Rotating方法里面是什么呢?
- void Rotating (float horizontal, float vertical)
- {
- // 创建一个Vector3用于保存输入的水平位移方向
- Vector3 targetDirection = new Vector3(horizontal, 0f, vertical);
-
- // 根据上面的方向计算这个方向指向的角度
- Quaternion targetRotation = Quaternion.LookRotation(targetDirection, Vector3.up);
-
- // 创建一个从玩家当前方向旋转到目标方向的旋转增量
- Quaternion newRotation = Quaternion.Lerp(rigidbody.rotation, targetRotation, turnSmoothing * Time.deltaTime);
-
- // 修改玩家的方向
- rigidbody.MoveRotation(newRotation);
- }
复制代码
代码没有实现玩家的位置移动的代码,但是实际运行的时候却可以看到角色会移动。这是为什么?
我们一起看到“Hierarchy”视图中的“char_ethan”。注意里面的Animator组件(它勾选了“Apply Root Motion”):
我们看到官方文档对于这个选项是这么介绍的:Root motion is the effect where an object's entire mesh moves away from its starting point but that motion is created by the animation itself rather than by changing the Transform position.
这里大致的意思其实就是:这个选项会在由动画产生移动时,使得物体的mesh会偏离起点。那也就是物体的移动是做在动画中的,然后勾选Animator的ApplyRootMotion选项来实现的。
关于角色控制中anim.SetFloat的方法的,这个涉及到动画混合,我们在这里暂时不讲。
那么角色的移动、旋转就都讲了。接下来我们看看碰撞器。
4、碰撞器及其使用
前面我们说了碰撞器分静态和动态两种。
那么角色“char_ethan”身上的肯定就是动态碰撞器了,注意到“char_ethan”对象有个“Capsule Collider”组件。这就是一个胶囊体碰撞器组件。然后角色还有个“rigidbody”组件,这个是刚体组件。也就是采用前面说的普通碰撞器+刚体组件。
关于各种内置碰撞器的知识。大家不了解的可以看看官网http://docs.unity3d.com/Manual/Physics3DReference.html(我不会告诉你我大部分U3D的知识都在官网学习的。)
这里只要知道碰撞器一个重要的选项“Is Trigger”
只要两个相互碰撞的碰撞器,有任何一个勾选了IsTrigger,那么就会触发这两个碰撞器上面的OnTriggerEnter方法。如果两个相互碰撞的碰撞器,都没勾选IsTrigger,那么会触发这两个碰撞器上面的OnCollisionEnter,OnCollisionStay,OnCollisionExit方法。
只有在不勾选IsTrigger的时候,才能使用Physics.Raycast进行射线检测。所以当你的碰撞方法没有调用的时候,请确保IsTrigger和方法是符合的。
注意:这个游戏中玩家走路的时候不会穿墙,因为墙的碰撞器和玩家身上的碰撞器都没有勾选IsTrigger。
5、怪物AI
在“Hierarchy”视图中找到三个怪物“char_robotGuard_001”、“char_robotGuard_002”、“char_robotGuard_003”,这三个怪物上面的组件其实都是一样的。我们来看看“char_robotGuard_001”就可以了。看看怪物是怎么巡逻、然后发现敌人进行追踪的。
怪物身上有如下组件:
A、Animator
这里不再赘述。
B、碰撞器
两个碰撞器?为什么这里要用两个碰撞器?我们先看看两个碰撞器的属性:
一个是胶囊体,没有勾选IsTrigger。这个应该是配合走路,为了不让角色穿过建筑物的。
第二个是球体碰撞器,勾选了IsTrigger。干嘛用的,别急,我们还是先来看那4个脚本吧。
C、DoneEnemySight
二话不说我们先上代码:
- using UnityEngine;
- using System.Collections;
-
- public class DoneEnemySight : MonoBehaviour
- {
- public float fieldOfViewAngle = 110f; // Number of degrees, centred on forward, for the enemy see.
- public bool playerInSight; // Whether or not the player is currently sighted.
- public Vector3 personalLastSighting; // Last place this enemy spotted the player.
-
-
- private NavMeshAgent nav; // Reference to the NavMeshAgent component.
- private SphereCollider col; // Reference to the sphere collider trigger component.
- private Animator anim; // Reference to the Animator.
- private DoneLastPlayerSighting lastPlayerSighting; // Reference to last global sighting of the player.
- private GameObject player; // Reference to the player.
- private Animator playerAnim; // Reference to the player's animator component.
- private DonePlayerHealth playerHealth; // Reference to the player's health script.
- private DoneHashIDs hash; // Reference to the HashIDs.
- private Vector3 previousSighting; // Where the player was sighted last frame.
-
-
- void Awake ()
- {
- // Setting up the references.
- nav = GetComponent<NavMeshAgent>();
- col = GetComponent<SphereCollider>();
- anim = GetComponent<Animator>();
- lastPlayerSighting = GameObject.FindGameObjectWithTag(DoneTags.gameController).GetComponent<DoneLastPlayerSighting>();
- player = GameObject.FindGameObjectWithTag(DoneTags.player);
- playerAnim = player.GetComponent<Animator>();
- playerHealth = player.GetComponent<DonePlayerHealth>();
- hash = GameObject.FindGameObjectWithTag(DoneTags.gameController).GetComponent<DoneHashIDs>();
-
- // Set the personal sighting and the previous sighting to the reset position.
- personalLastSighting = lastPlayerSighting.resetPosition;
- previousSighting = lastPlayerSighting.resetPosition;
- }
-
-
- void Update ()
- {
- // If the last global sighting of the player has changed...
- if(lastPlayerSighting.position != previousSighting)
- // ... then update the personal sighting to be the same as the global sighting.
- personalLastSighting = lastPlayerSighting.position;
-
- // Set the previous sighting to the be the sighting from this frame.
- previousSighting = lastPlayerSighting.position;
-
- // If the player is alive...
- if(playerHealth.health > 0f)
- // ... set the animator parameter to whether the player is in sight or not.
- anim.SetBool(hash.playerInSightBool, playerInSight);
- else
- // ... set the animator parameter to false.
- anim.SetBool(hash.playerInSightBool, false);
- }
-
-
- void OnTriggerStay (Collider other)
- {
- // If the player has entered the trigger sphere...
- if(other.gameObject == player)
- {
- // By default the player is not in sight.
- playerInSight = false;
-
- // Create a vector from the enemy to the player and store the angle between it and forward.
- Vector3 direction = other.transform.position - transform.position;
- float angle = Vector3.Angle(direction, transform.forward);
-
- // If the angle between forward and where the player is, is less than half the angle of view...
- if(angle < fieldOfViewAngle * 0.5f)
- {
- RaycastHit hit;
-
- // ... and if a raycast towards the player hits something...
- if(Physics.Raycast(transform.position + transform.up, direction.normalized, out hit, col.radius))
- {
- // ... and if the raycast hits the player...
- if(hit.collider.gameObject == player)
- {
- // ... the player is in sight.
- playerInSight = true;
-
- // Set the last global sighting is the players current position.
- lastPlayerSighting.position = player.transform.position;
- }
- }
- }
-
- // Store the name hashes of the current states.
- int playerLayerZeroStateHash = playerAnim.GetCurrentAnimatorStateInfo(0).nameHash;
- int playerLayerOneStateHash = playerAnim.GetCurrentAnimatorStateInfo(1).nameHash;
-
- // If the player is running or is attracting attention...
- if(playerLayerZeroStateHash == hash.locomotionState || playerLayerOneStateHash == hash.shoutState)
- {
- // ... and if the player is within hearing range...
- if(CalculatePathLength(player.transform.position) <= col.radius)
- // ... set the last personal sighting of the player to the player's current position.
- personalLastSighting = player.transform.position;
- }
- }
- }
-
-
- void OnTriggerExit (Collider other)
- {
- // If the player leaves the trigger zone...
- if(other.gameObject == player)
- // ... the player is not in sight.
- playerInSight = false;
- }
-
-
- float CalculatePathLength (Vector3 targetPosition)
- {
- // Create a path and set it based on a target position.
- NavMeshPath path = new NavMeshPath();
- if(nav.enabled)
- nav.CalculatePath(targetPosition, path);
-
- // Create an array of points which is the length of the number of corners in the path + 2.
- Vector3 [] allWayPoints = new Vector3[path.corners.Length + 2];
-
- // The first point is the enemy's position.
- allWayPoints[0] = transform.position;
-
- // The last point is the target position.
- allWayPoints[allWayPoints.Length - 1] = targetPosition;
-
- // The points inbetween are the corners of the path.
- for(int i = 0; i < path.corners.Length; i++)
- {
- allWayPoints[i + 1] = path.corners[i];
- }
-
- // Create a float to store the path length that is by default 0.
- float pathLength = 0;
-
- // Increment the path length by an amount equal to the distance between each waypoint and the next.
- for(int i = 0; i < allWayPoints.Length - 1; i++)
- {
- pathLength += Vector3.Distance(allWayPoints[i], allWayPoints[i + 1]);
- }
-
- return pathLength;
- }
- }
复制代码
Awake | 主要是获取一些组件,在后面的逻辑中可以使用。 | Update | 检查玩家最后一次被发现的位置是否一致,不是则同步位置。
把playerInSight(是否发现玩家)这个状态设置到Animator的状态机里面。 | OnTriggerStay | 没错,前面说到的第二个碰撞器正是这里要用到的。第二个碰撞器勾选了IsTrigger,就是为了触发这个方法。
当玩家进入这个碰撞器(也就是怪物的视野)触发。
检查玩家是否在怪物前方,而且玩家和怪物中间没有遮挡物。那么设置playerInSight为true。 | OnTriggerExit | 当玩家离开怪物的视野(碰撞器)。那么设置playerInSight为false。 | CalculatePathLength | 计算怪物和玩家当前的距离。 |
基本这个组件就是用于检测玩家是否在怪物视野内。然后设置玩家的一些位置信息,以及是否被发现的信息。
我们接着看DoneEnemyAI这个组件
D、DoneEnemyAI
还是上代码:
- using UnityEngine;
- using System.Collections;
-
- public class DoneEnemyAI : MonoBehaviour
- {
- public float patrolSpeed = 2f; // The nav mesh agent's speed when patrolling.
- public float chaseSpeed = 5f; // The nav mesh agent's speed when chasing.
- public float chaseWaitTime = 5f; // The amount of time to wait when the last sighting is reached.
- public float patrolWaitTime = 1f; // The amount of time to wait when the patrol way point is reached.
- public Transform[] patrolWayPoints; // An array of transforms for the patrol route.
-
-
- private DoneEnemySight enemySight; // Reference to the EnemySight script.
- private NavMeshAgent nav; // Reference to the nav mesh agent.
- private Transform player; // Reference to the player's transform.
- private DonePlayerHealth playerHealth; // Reference to the PlayerHealth script.
- private DoneLastPlayerSighting lastPlayerSighting; // Reference to the last global sighting of the player.
- private float chaseTimer; // A timer for the chaseWaitTime.
- private float patrolTimer; // A timer for the patrolWaitTime.
- private int wayPointIndex; // A counter for the way point array.
-
-
- void Awake ()
- {
- // Setting up the references.
- enemySight = GetComponent<DoneEnemySight>();
- nav = GetComponent<NavMeshAgent>();
- player = GameObject.FindGameObjectWithTag(DoneTags.player).transform;
- playerHealth = player.GetComponent<DonePlayerHealth>();
- lastPlayerSighting = GameObject.FindGameObjectWithTag(DoneTags.gameController).GetComponent<DoneLastPlayerSighting>();
- }
-
|
|