赞
踩
Unity是一个游戏引擎(类似于软件开发的框架),它包含渲染引擎,物理引擎,碰撞检测,音效,动画效果,场景管理等功能,可以开发2d游戏,一般是用来开发3d游戏。它的开发效率高、脚本使用C#开发、简单易用、跨平台(可以导出各个平台的程序)。当然还有别的游戏引擎比如虚幻引擎俗称UE (Unreal Engine
),使用C++开发脚本,3A大作首选,好多游戏公司都有自己的游戏引擎,下边放一个Unity软件的大图。本人感觉,这个软件还是比较好学习的,比单纯的软件开发有意思的多。
transform
组件Assets
是最重要的,别的可以不要unity hub
(此软件用来管理多个unity editor)ProjectSettings\ProjectVersion.txt
查看项目的unity版本视图有两种模式,ISO正交模式(2d效果)和 Pers透视模式(近大远小效果)。
场景 scene,是相关联的一组游戏对象的集合,比如一个地图或者游戏的某一关,在scenes文件下,后缀名是 .unity
双击此文件可打开项目。新建的游戏对象,默认在当前视图的场景中间。场景默认自带Main Camera
,和Directional Light
两个东西。
游戏对象,hierachy中的每个对象都是GameObject的子类,都有transform属性。Plane只有正面没有反面,quad相似,只不过是竖着的。任何物体都是三角形拼出来的。
如果在Hierachy面板中,要把一个游戏对象变成模板,直接拖到 Project面板即可,修改PreFab的属性就会影响所有的游戏对象。如果要批量创建模型,最好放到预制件里。
如图,如果要复制这些参数给别的组件使用,可以点这里,选择 copy componnet,
然后在新的组件再点这里,选择 Paste Component Values。
比如,如果要保存这个transform的参数,给别的项目使用或者给别的组价使用,可以点这里
点save,会保存为一个文件
可以在别的组件直接使用这个transform的参数,而不用手动输入了。
创建脚本,不用的方法,最好删除。
每个脚本的执行没有顺序的,脚本可以设置执行优先级,选中脚本,然后点击右侧按钮。
可以选择一个脚本作为主控脚本。
脚本的参数,要 public才行,也可以添加 tooltip
public class Planet : MonoBehaviour { [Tooltip("帧数")] public int frame = 60; void Start() { Application.targetFrameRate = frame; } void Update() { this.transform.Rotate(0, 30 * Time.deltaTime, 0); } }
参数设置优先级, 默认 < 编辑器 < Awake < start
网格,mesh filter
可以改变物体的形状。
Mesh Renderer
渲染物体,可以改变材质,两个组合在一起才能显示物体。可以创建空物体,自行添加,这两个组件,让物体显示。
如果要组合两个对象,在hierachy
面板,一个拖到另一个就可以。也可以创建几个空模型作为父对象,其他的拖进来。这时候子模型的坐标就是相对坐标了,相对父模型的坐标,可以使用transform
组件的reset
功能重置,会更好。如果后续模型更新,如果把组件都挂在模型上,就要重新替换,所以建议新建一个空物体作为父模型,把所有行为都给父模型即可。
物体的质地,比如色彩、纹理、透明度等,实际就是shader的实例。
纹理(texture)就是附加到物体表面的贴图。材质要给到 Mesh Renderer
。
在Assets目录下新建Textures目录,放置贴图图片,可以直接拖到game object上,unity会自动生成一个材质。
Albedo
基础贴图,决定物体纹理和颜色
Rendering mode
材质的渲染模式 ( Rendering mode
) 默认是 opaque(不透明的),fade(渐变,淡入淡出)
cutput (镂空,透明通道去掉,只剩下不透明的)
transparent(透明的,需要设置albedo的颜色的a值)
Metallic
使用金属模拟外观
Specular
镜面反射
Smothless
光滑度,设置物体表面光滑程度
Normal Map
法线贴图,物体表面凹凸程度
Emission
自发光,控制物体表面自发光颜色和贴图。
材质的本质是shader的实例,shader是专门用来渲染3d图形的技术,可以使纹理以某种方式展现,就是一种嵌入到渲染管线的程序,控制GPU运行效果的算法。材质的属性和设置,是由shader决定的。
Shaded着色模式(默认模式)所有游戏对象的贴图都正常显示
Wireframe网格线框显示模式以网格线框形式显示所有对象
Shaded Wireframe着色模式线框以贴图加网格线框形式显示对象
Shadow Cascades阴影级联以阴影方式显示对象
Render Paths渲染路径显示模式以渲染路径的形式显示
Alpha ChannelAlpha通道显示以灰度图的方式显示所有对象
Overdraw以半透明方式显示以半透明的方式显示所有对象
MipmapsMIP映射图显示以MIP映射图方式显示所有对象
平行光,像太阳,从一个方向过来照亮全景
点光,一个点发射光线,并减弱
聚光,某个方向照射,有范围限制
范围光,一个区域内的光,没有方向
实时光照和烘焙光照,后者就是开发时把光照计算好,在游戏运行时,直接使用图片产物,提升性能。
一般场景为了真实,会有几个光源,因为有漫反射的存在,没有绝对黑的物体的面。
很多游戏没有阴影,因为实时渲染影响非常消耗资源。
比如范围灯光,看不出效果,可以烘焙看一下,windows -> rendering -> lighting
可以实现硝烟,火焰,雪花,水汽,爆炸效果。右键Effects -> Particle System来创建。也可以从网上下载现成的使用。
渲染管线执行一系列操作来获取场景的内容,并将这些内容显示在屏幕上。
在 Unity 中,可以选择不同的渲染管线。Unity 提供了三个具有不同功能和性能特征的预构建渲染管线,您也可以创建自己的渲染管线。
渲染3D物体需要考虑物体着色、光照计算、环境光、阴影计算,、传递哪些数据、 自定义的Shader等,渲染管线,就是定义了处理这些的原理原则,在这些原则下渲染游戏场景中的物体。
Unity的渲染管线分为3大类渲染管线: 1是向前渲染管线,2是延时渲染管线,3是可编程渲染管线; 1和2是Unity引擎内置的渲染管线,来处理整个场景的绘制和渲染相关机制,3允许开发者自己定义,这样能获得更好的灵活性和效率。
比如向前渲染管线,每个重要光源,都要经过一次Pass,来渲染一次物体,不重要的光源,在ForwardBase里面计算等,而延时渲染就是先把所有的光照计算出来,最后再渲染(“延时”)。不同的渲染管线,对应的技术处理的策略不一样。
1、默认渲染管线。默认渲染管线就是你不装LWRP、UPR、HDRP这些管线,Unity本来就内置的管线。这个管线只是Unity对底层渲染API(如OpenGL等)做了简单封装搞出来的,并且当时Unity并没有去考虑这个管线的可定制性。尽管可以用CommandBuffer和一些渲染时的回调来对这个管线的渲染流程做一些修改,但个人感觉都是些比较骚的操作,搞出来的东西稳定性不是很好。所以默认渲染管线的问题就是死板。
2、SRP。即可编程渲染管线。这玩意相当于是对默认渲染管线又做了层封装,并暴露给用户。这大大提高了渲染管线的可定制性。SRP把渲染管线细分成一个个模块和过程,你只需要按照自己的需求去组装和重写即可。比如我用SRP可以先渲染出场景中一部分物体,然后做个后处理,再渲染剩下的物体,在做其它后处理等等。
2、LWRP。这玩意就是Unity用SRP给你写的一个渲染管线。如果用户觉得自己没有太多的渲染管线定制需求,也不想花很多时间在管线的编写上的用户,那直接就可以用这个LWRP来替代默认渲染管线。并且LWRP也具有一定的可定制性。
3、URP。就是LWRP的升级版。给LWRP改改BUG、优化些体验、用些底层新特性、提高渲染质量这样。
///
会被编译,//
不会。所以使用///
会减慢编译的速度(但不会影响执行速度)且在其它的人调用你的代码时提供智能感知。定义,有方向和长度的量,叫做向量(数学学的)。Unity里有Vector2和Vector3等。
一般使用Vector3,两个向量可以直接相加减。
Vector3 v = new Vector3(0,1,2);
// 向量长度
v.magnitude;
单位向量,长度为1的向量。
// 标准化v,返回v1是单位向量
var v1 = v.normalized;
常用的常量比如 vector3.forward
代表 (0,0,1)等
求两个向量的距离vector3.Distance
GameObject是游戏场景中真实存在的,而且有位置的一个物件。Component附属于GameObject,控制GameObject的各种属性。GameObject是由Component组合成的,Component的生命周期和GameObject息息相关。调用此GameObject的Destroy方法,它的子对象和对应的所有Component都会被销毁,但也可以一次只销毁一个Component。常用的组件如下表所示
Component | 作用 |
---|---|
RigidBody 刚体 | 使物体能在物理控制下运动 |
Collider 碰撞器 | 和RigidBody刚体一起使碰撞发生,没有Collider,两个碰撞的刚体会相互穿透 |
Renderer 渲染器 | 使物体显示在屏幕上 |
AudioSource 音频源 | 使物体在scence场景播放音频 |
Animation 动画 | |
Animator 动画控制器 |
脚本也是组件,因此能附到游戏对象上。常用的组件可以通过简单的成员变量获取,附在游戏对象上的组件或脚本可以通过GetComponent获取。
using UnityEngine;
using System.Collections;
public class example : MonoBehaviour {
void Awake() {
transform.Translate(0, 1, 0);
GetComponent<Transform>().Translate(0, 1, 0);
}
}
获取某个组件,可以直接把引用给脚本即可。
MonoBehaviour
public class WeatherParticle : MonoBehaviour
下边是一个脚本的demo
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Ball : MonoBehaviour { // 游戏开始运行的时候执行,适合做初始化 void Start() { Debug.Log("组件执行开始!"); transform.position = new Vector3(1, 2.5f, 3); } // 每帧都会执行,不同设备的频率不一样 void Update() { Debug.Log("当前游戏进行时间:" + Time.time); // 每次帧移动 1.5 transform.Translate(1.5f, 0, 0) // 如果要保证每秒移动距离相同,Time.deltaTime是两个帧的间隔 transform.Translate(1.5 * Time.deltaTime, 0, 0) } }
获取位置
// 获取纵轴输入和横轴输入
// wsad和上下左右键的输入
float v = Input.GetAxis("Vertical");
float h = Input.GetAxis("Horizontal");
Debug.Log("纵轴输入"+v+"横轴输入" + h);
transform.Translate(0, h * 3 * Time.deltaTime, v * 3 * Time.deltaTime);
触发和碰撞
// 1. 增加一个立方体,在 box collider上勾选 is trigger // 2. 给球体增加一个刚体组件,(add component -> pyhsicis -> rigidbody)勾选 // 3. 使用前边的小球撞立方体,给立方体挂一个脚本 using System.Collections; using System.Collections.Generic; using UnityEngine; public class Coin : MonoBehaviour { //触发开始事件OnTriggerEnter private void OnTriggerEnter(Collider other) { Debug.Log(other.name + "碰到了"); } private void OnTriggerStay(Collider other) { Debug.Log(other.name + "碰撞持续中"); } private void OnTriggerExit(Collider other) { Debug.Log(other.name + "碰撞结束"); } }
源代码 - CLS - 中间语言 - Mono Runtime - 机器码
打开编辑器的目录,比如我的是
D:\apps\unity-editor\2021.3.29f1c1\Editor\Data\Resources\ScriptTemplates
打开 81-C# Script-NewBehaviourScript.cs.txt
修改即可
也叫必然事件/消息,脚本从唤醒到销毁的过程。附加到游戏对象上,控制游戏行为。
在class中声明的可见的成员变量(private不可见),在unity软件中可见也可以修改,并且这个优先级高,以为在unitu中这是一个对象。不要在脚本中写构造函数。
public class Demo1 : MonoBehaviour { // 加上这个,虽然是public在unity也不可见 [HideInInspector] public float speed = 10; // 虽然私有,但是在unity可见 [SerializeField] private float speed1 = 10; // 字段定义数值范围 [Range(0, 100)] public float speed2 = 10; // 执行时机,渲染帧执行,每次渲染时执行,执行间隔不固定和设备性能和渲染量有关 // 适用性: 处理游戏逻辑 void Update() { float v = Input.GetAxis("Vertical"); float h = Input.GetAxis("Horizontal"); transform.Translate(h * speed * Time.deltaTime, 0, v * speed * Time.deltaTime); } // 固定时间更新, // 适用性:适用游戏对象做物理操作,比如移动等,默认为0.02s,不会受到渲染影响 private void FixedUpdate() { } // 延迟更新,适用于跟随逻辑 private void LateUpdate() { } // 开始时被调用 // 创建游戏物体,立即执行 // 在脚本被禁言仍然可以执行 private void Awake() { Debug.Log("awake" + Time.time); print("lll"); } // Awake之后执行 // 创建游戏物体,脚本启用才执行 // 如果此脚本被挂在多个游戏对象上,先执行所有的awake,再执行所有的start // this.name 游戏对象的名字 private void Start() { print(this); speed = 10; Debug.Log("Start" + Time.time + this.name); // unity内置随机数 int b = Random.Range(0, 100); int a = 0; while(a < -1){ //我们将obj1初始化为一个Cube立方体,当然我们也可以初始化为其他的形状 GameObject obj1 = GameObject.CreatePrimitive(PrimitiveType.Cube); //设置物体的位置Vector3三个参数分别代表x,y,z的坐标数 obj1.transform.position = new Vector3((float)(1 + a * 1.3),1,1); //给这个创建出来的对象起个名字 obj1.name = ("dujia" + a); a++; } //设置物体的tag值,在赋值之前要在Inspector面板中注册一个tag值 //注册tag值得方法,用鼠标选中摄像机对象在Inspector面板中找到tag,选addtag // obj1.tag = "shui"; //设置物体贴图要图片文件放在(Resources)文件夹下,没有自己创建 // obj1.renderer.material.mainTexture = (Texture)Resources.Load("psb20"); } private void OnMouseEnter() { Debug.Log("OnMouseEnter"); } private void OnMouseExit() { Debug.Log("OnMouseExit"); } private void OnMouseDown() { // print(this.activeSelf); print("OnMouseDown"); } private void OnMouseUp() { Debug.Log("OnMouseUp"); } // 当物体在相机不可见时 private void OnBecameInvisible() { } // 当物体在相机可见时 private void OnBecameVisible() { } // 销毁时 private void OnDestroy() { } // 程序结束时 private void OnApplicationQuit() { } }
比如点击屏幕,让cube移动,cube上有个脚本 cube.cs
,上面有move1
的方法,另外一个脚本 control.ts
有cube这个游戏对象的引用,那么就可以直接这么写
cube.sendMessage("move1");
如果没有这个方法名会报错。
private void Awake()
{
Debug.Log("awake" + Time.time);
print("123");
}
public class ComDemo : MonoBehaviour { // OnGUI是Unity中通过代码驱动的GUI系统 // 主要用来创建调试工具、创建自定义属性面板、创建新的Editor窗口和工具达到扩展编辑器效果 private void OnGUI() { // 左上角创建一个按钮 if(GUILayout.Button("click")){ // 点击按钮的行为 print("ok"); this.GetComponent<MeshRenderer>().material.color = Color.blue; // 自定义颜色 // this.GetComponent<MeshRenderer>().material.color = new Color(0.3f, 0.4f, 0.6f, 0.3f); // 获取所有的组件 var all = this.GetComponents<Component>(); foreach (var item in all) { print(item.GetType()); } // 深度优先搜索所有的子组件 // var allAndChildren = this.GetComponentsInChildren<MeshRenderer>(); } } }
public class Move1 : MonoBehaviour { private void Start() { // 建议unity一个固定的帧率,不一定有效 Application.targetFrameRate = 60; } void Update() { // 直接给一个新的位置 var ps = transform.localPosition; ps.x += 1.2f * Time.deltaTime; transform.localPosition = ps; print(Time.deltaTime); /** 相对移动,传入位移增量 */ // 自身坐标系z轴每次移动1米 this.transform.Translate(0, 0, 1); // 世界坐标系 this.transform.Translate(0, 0, 1, Space.World); } }
using UnityEngine; public class RotateDemo : MonoBehaviour { void Start() { Application.targetFrameRate = 60; // 官方推荐,使用欧拉角设置旋转 // transform.localEulerAngles = new Vector3(0, 45f, 0); // transform.rotation = Quaternion.Euler(3.0f, 1.2f, 1.0f); } void Update() { // 使用欧拉角设置 // var angle = transform.localEulerAngles; // angle.y += 30 * Time.deltaTime; // transform.localEulerAngles = angle; // 自身坐标系的y轴旋转10度 this.transform.Rotate(0,30 * Time.deltaTime,0); // this.transform.Rotate(0, 10, 0, Space.World); } }
public class TransformDemo : MonoBehaviour { void OnGUI() { if (GUILayout.Button("transform")) { // 每个子物体的变化组件 foreach (Transform c in transform) { print(c.name); } // 世界坐标系坐标位置 print(this.transform.position); // 相对于父轴心点的位置 print(this.transform.localPosition); // 相对于父的缩放 print(this.transform.localScale); // 围绕世界坐标点旋转,y轴,1度 this.transform.RotateAround(Vector3.zero, Vector3.up, 1); // 获取父物体变换组件 this.transform.parent // 是最高层级的transform this.transform.root // 设置父物体变换组件 this.transfrom.setParent(this.transform.root, true) // 根据名称查找子物体的transform对象 Transform tf1 = this.transform.Find("trans1"); // 查找孙子路径 Transform tf11 = this.transform.Find("trans1/transf2"); int count = this.transform.childCount; print(count); for(int i=0;i < count; i++){ print(transform.GetChild(i)); } } } }
勾上就是激活,不勾上就是不激活状态,可以用setActivate来设置,不激活就是不在场景显示了。this.activeSelf
是自己的激活状态,但是如果父物体不是,那也不是。所以使用 activeInHierarchy
来判断最终状态。
public class GameObject1 : MonoBehaviour { void OnGUI() { if (GUILayout.Button("GameObject1")) { print(this.gameObject.activeSelf); print(this.gameObject.activeInHierarchy); // this.gameObject.SetActive(false); // 所有的组件不能new Light light = this.gameObject.AddComponent<Light>(); light.color = Color.red; light.type = LightType.Point; // 使用标签或者游戏物体列表 GameObject[] gbs = GameObject.FindGameObjectsWithTag("Player"); GameObject gb = GameObject.FindWithTag("Player"); print(this.gameObject.name); // 5s后销毁游戏对象 Destroy(this.gameObject, 5); } } }
可以获取用户的输入事件
using UnityEngine; // 这个就会在 add component下显示一个Demo2222的文件夹,名字是input111 // 用于定义在unity中的别名 // 这种写法是C#的特性 [AddComponentMenu("Demo2222/input111")] public class InputDemo : MonoBehaviour { void Start() { } void Update() { if (Input.GetKey(KeyCode.A)) { print("Pressed A."); } if(Input.GetKeyDown(KeyCode.W)){ print("Pressed Down w."); } // 鼠标被按下,不停的出发 if (Input.GetMouseButton(0)){ print("Pressed left click."); } // 用户按下了左键,只触发一次 if (Input.GetMouseButtonDown(0)){ print("Pressed left down."); } // 用户抬起了左键,只触发一次 if (Input.GetMouseButtonUp(0)){ print("Pressed left Up."); } if (Input.GetMouseButton(1)){ print("Pressed right click."); } if (Input.GetMouseButton(2)){ print("Pressed middle click."); } // Fire1的定义如下所示 if (Input.GetButton("Fire1")) { print("fire1"); } // 这种down的执行的次数少 if (Input.GetButtonDown("Fire1")) { print("fire1 down"); } // 参数值也是标准输入选的,在setting里 // print(Input.GetAxis("Horizontal")); // 没有中间过程的值,比如按下 w使得 0 - 1 上边方法可能有小数,这个要么0要么1 // print(Input.GetAxisRaw("Horizontal")); // 1 print(Input.GetAxis("Fire1")); } }
Edit -> Project Settings -> Input Manager 有一些输入的自定义设置,
2019年推出的系统,设备和动作分离,比老板复杂,分为Input Action、input signal bindings、Devices。
这里设置为新输入系统。
在assets目录新建settings目录,再新建 Input Actions
点击 edit asset
创建一个移动的action,配置 action type是value,control type是 vector2,并添加WASD的键盘绑定。
创建 control schema
并在这里勾选
也可以让unity自动生成配置,在add component的列表中
这里Behavior可以选择 Invoke Unity Events,可以绑定脚本事件
也可以输入代码方式,调用 PlayerInput
using UnityEngine; using UnityEngine.InputSystem; public class InputDemo : MonoBehaviour { // PlayerControl 是自动生成的脚本的名字 public PlayerControl inputControl; public Vector2 dir; void Awake() { inputControl = new PlayerControl(); } private void OnEnable() { inputControl.Enable(); } private void OnDisable() { inputControl.Disable(); } void Update() { dir = inputControl.Player.Move.ReadValue<Vector2>(); print(dir.x); } public void Move1() { print("moving...."); } }
using System.Collections; using System.Collections.Generic; using UnityEngine; // Time.deltaTime -> Time.unscaledDeltaTime // Time.time -> Time.unscaledTime === Time.realtimeSinceStartup (游戏真实运行时间) public class TimeDemo : MonoBehaviour { // Update不受timeScale影响,但是加上了Time.deltaTime就被影响了 // unscaledDeltaTime不受缩放影响的每帧间隔 private void Update() { // this.transform.Rotate(0, 100 * Time.deltaTime, 0); // this.transform.Rotate(0, 100 * Time.unscaledDeltaTime, 0); } private void FixedUpdate() { // this.transform.Rotate(0, 100, 1); } private void OnGUI() { if (GUILayout.Button("Time")) { // 游戏开始的秒数 print(Time.time); // 每帧的间隔时间 0.004 print(Time.deltaTime); // 可以保证在机器性能不一样的情况下,保存转速恒定 // 机器好的情况下,渲染速度快 Time.deltaTime小,反之,渲染速度慢,Time.deltaTime大 this.transform.Rotate(0, 100 * Time.deltaTime, 1); } if (GUILayout.Button("暂停游戏")) { Time.timeScale = 0; } if (GUILayout.Button("继续游戏")) { Time.timeScale = 1; } } }
资源要放到Assets/Resources目录, 使用 Resources.Load
方法加载,如果是加载FBX模型是GameObject对象,但是不会再Hierachy面板出现,需要Instantiate
复制一个实例才能显示出来。
public class ResourcesLoad : MonoBehaviour
{
// 按下w播放音乐
void Update()
{
if (Input.GetKeyDown(KeyCode.W))
{
// ,不需要写扩展名
var music = Resources.Load("Audios/bg");
AudioSource.PlayClipAtPoint(music as AudioClip, Camera.main.transform.position);
}
}
}
新建两个场景 Scene1 和 Scene2,拖拽到BuildSetting面板中,注意,这个顺序就显示的顺序
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.SceneManagement; public class ToggleScene : MonoBehaviour { public void Scene1(){ // 新版切换场景方法 SceneManager.LoadScene("Scene1"); // 老版切换场景方法,已废弃 Application.LoadLevel("Scene1"); } public void Scene2(){ SceneManager.LoadScene("Scene2"); } }
一般都是 MP3和WAV文件,AudioSource
播放组件,AudioListener
收听音乐,摄像机默认都有一个AudioListener
。
在Camera新增一个AudioSource
,并添加音乐到 AudioClip中。
public class AudioControl : MonoBehaviour { private AudioSource a; void Start() { this.a = GetComponent<AudioSource>(); } void Update() { if (Input.GetKeyDown(KeyCode.Q)) { // this.a.Play(); // 剪辑,位置,音量 AudioSource.PlayClipAtPoint(this.a.clip, Camera.main.transform.position, 0.1f); } } }
动画的使用方式如下。
也可以点击curves,直接操作曲线
选中右侧的点,录制开始和结束状态,就可以播放动画了,左键+alt可以拖动动画面板
录制好之后,在inspector面板 Animation组件的Animation属性,选中这个动画就可以播放了。
新建脚本
public class DoorControl : MonoBehaviour{ public bool open = false; public Animation animation1; public string animationName = "Door"; void Start(){ this.animation1 = this.GetComponent<Animation>(); } void OnMouseDown(){ if(!this.animation1.isPlaying){ if(open){ // 倒着播放 this.animation1[animationName].speed = -1; // 没有这句话,就是 0 - 0了,门瞬间关闭了,从最后开始播放 this.animation1[animationName].time =this.animation1[animationName].length; }else { this.animation1[animationName].speed = 1; } this.animation1.Play(animationName); open = !open; } } }
动画播放模式
loop就是循环播放,once一次,pingpang就是来回回来播放,clapforver固定播放到最后一帧(一直播放中)
动画机含有多个动画的片段,并使用controller通过parameter控制。可以在Project面板右键Animator Controller来创建。
右键 Make transition可以连到下一个动画,点击线可以添加切换的条件,连线可以选中点击delete删除,条件的值可以在脚本设置。
类似浏览器的LocalStorage,但是不存在文件系统,只有游戏卸载了才不存在。
public class PlayerPrefsTest : MonoBehaviour { void Update() { if (Input.GetKeyDown(KeyCode.Z)) { PlayerPrefs.SetInt("level", 11); PlayerPrefs.SetFloat("level1", 11f); PlayerPrefs.SetString("level11", "11"); } } private void OnMouseDown() { print(PlayerPrefs.GetInt("level")); } }
当前代码,在等待某些资源条件好的时候,在未来执行。
public class CoRoutineTest : MonoBehaviour { void Update() { if(Input.GetKeyDown(KeyCode.L)){ StartCoroutine(play1()); } } // 2秒后再print(2) IEnumerator play1(){ print(1); yield return new WaitForSeconds(2); print(2); } }
基于HTTP的网络传输功能,需要结合协同使用。
public class WWWDemo : MonoBehaviour { private Texture2D pic; void Start(){ } void Update() { if(Input.GetKeyDown(KeyCode.O)){ StartCoroutine(DrawPic()); } } void OnGUI(){ if(pic != null){ GUI.DrawTexture(new Rect(0,0,100,100), pic); } } IEnumerator DrawPic(){ WWW w3 = new WWW("https://img0.baidu.com/it/u=3111329697,2164934529&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=313"); yield return w3; pic = w3.texture; } }
可以用WWW或者HttpWebRequest来实现文件的下载。因为WWW不存在设置timeout属性,因此当我们网络不好请求超时的时候,无法简单的做出判断。当网络极差的时候,游戏下载将会停止(即一直在等待yield return www)当时间较长时网络恢复将无法继续下载,也没有提示,需要重启才能重新下载。Unity早在5.4版本的时候就出了新的API UnityWebRequest用于替代WWW。
using System.Collections; using UnityEngine; using UnityEngine.Networking; public class HttpTest : MonoBehaviour { private string jsonUrl = "http://localhost:8888/demo.json"; private void Start() { StartCoroutine(Get()); } IEnumerator Get() { UnityWebRequest request = UnityWebRequest.Get(jsonUrl); yield return request.SendWebRequest(); if(request.isHttpError || request.isNetworkError) { Debug.LogError(request.error); } else { string receiveContent = request.downloadHandler.text; Debug.Log(receiveContent); } } }
第一人称,把camera直接拖到物体内部并且position一致。
第三人称,把camera直接拖到物体内部而且位置不一样,可以看到自己。
如下,实现了前后左右移动的效果
public class CubeMove : MonoBehaviour { private float speed; private float angleSpeed; void Start(){ speed = 1f; angleSpeed = 5f; } void Update() { transform.Translate(Vector3.forward * speed * Time.deltaTime * Input.GetAxisRaw("Vertical")); transform.Rotate(Vector3.up, Input.GetAxisRaw("Horizontal") * angleSpeed * Time.deltaTime ); } }
Streaming Assets
,就是项目中的streamingAssets
目录,可以使用Application.streamingAssetsPath
得到,Unity 中的大多数资源在构建时都会合并到项目中。但是,将文件放入目标计算机上的普通文件系统以使其可通过路径名访问有时会很有用。Application.dataPath
就是项目的Assets目录。UnityEngine自带JsonUtility来处理json数据。
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEditor; using System; public class demo11 { [MenuItem("Excel/Json")] static void LoadJson() { Data data = new Data(); data.name = "Data"; data.subData.Add(new SubData(){ intValue = 1, boolValue = true, floatValue = 0.1f, stringValue = "one" }); data.subData.Add(new SubData(){ intValue = 2, boolValue = true, floatValue = 0.1f, stringValue = "two" }); string json = JsonUtility.ToJson(data); Debug.Log(json); data = JsonUtility.FromJson<Data>(json); Debug.LogFormat("name = {0}", data.name); foreach(var item in data.subData) { Debug.LogFormat("intValue = {0} boolValue = {0} floatValue = {0}stringValue = {0}", item.intValue, item.boolValue, item.floatValue, item.stringValue); } } [Serializable] public class Data { public string name; public List<SubData> subData =new List<SubData>(); } [Serializable] public class SubData { public int intValue; public bool boolValue; public float floatValue; public string stringValue; } }
Unity的JSON是不支持字典的,不过可以继承ISerializationCallbackReceiver接口,间接地实现字典序列化。
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEditor; public class Script_08_04 { [MenuItem("Excel/Load Dictionary")] static void SerializableDictionary() { SerializableDictionary<int,string> serializableDictionary = new SerializableDictionary<int,string>(); serializableDictionary [100] = "demo1"; serializableDictionary [200] = "200"; serializableDictionary [300] = "3000"; string json = JsonUtility.ToJson(serializableDictionary); Debug.Log(json); serializableDictionary = JsonUtility.FromJson<SerializableDictionary<int, string>>(json); Debug.Log(serializableDictionary [100]); } }
using System.Collections; using System.Collections.Generic; using UnityEngine; public class SerializableDictionary<K, V> : ISerializationCallbackReceiver { [SerializeField] private List<K> m_keys; [SerializeField] private List<V> m_values; private Dictionary<K, V> m_Dictionary = new Dictionary<K, V>(); public V this[K key] { get{ if(!m_Dictionary.ContainsKey(key)) return default(V); return m_Dictionary[key]; } set{ m_Dictionary [key] = value; } } public void OnAfterDeserialize() { int length = m_keys.Count; m_Dictionary = new Dictionary<K, V>(); for(int i = 0; i < length; i++) { m_Dictionary[m_keys[i]] = m_values[i]; } m_keys = null; m_values = null; } public void OnBeforeSerialize() { m_keys = new List<K>(); m_values = new List<V>(); foreach(var item in m_Dictionary) { m_keys.Add(item.Key); m_values.Add(item.Value); } } }
TextAsset是Unity 提供的一个文本对象,它可以通过Resources.Load 或者 AssetBundle 来读取数据,其中数据是string格式的。例如在resources目录下有demo.txt和 demo1.json两个文件。
using UnityEditor; using UnityEngine; public class LoadTextAsset { [MenuItem("MyTextAsset/LoadTextAsset", false, 0)] static void LoadMyTextAsset() { string mystr = Resources.Load<TextAsset>("demo").text; string mystr1 = Resources.Load<TextAsset>("demo1").text; Debug.Log(mystr); Debug.Log(mystr1); } }
使用C# 的File类来读写某个目录的文本,注意编辑器模式下读写文本是可以的,但是一旦打包发布,Assets/目录都不存在了,运行时是无法读取它目录下的文本的。
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEditor; using System.IO; using System.Text; public class Demo11 { [MenuItem("Tool/File")] static void Start() { string path = Path.Combine(Application.dataPath, "test.txt"); //如果文件存在,就删除它 if(File.Exists(path)) { File.Delete(path); } //写入文件 StringBuilder sb = new StringBuilder(); sb.AppendFormat ("第一行:{0}", 100).AppendLine(); sb.AppendFormat ("第二行:{0}", 200).AppendLine(); File.WriteAllText(path,sb.ToString()); //读取文件 Debug.Log(File.ReadAllText(path)); } }
Resources
目录用来读取游戏资源,在编辑器模式下可见,打包后运行不可见。StreamingAssets
目录可使用File类来读取文件(除了个别平台外),但都是只读的并不能写,StreamingAssets在打包后整个目录后保存的在本项目中。persistentDataPath
目录是可读可写的,这个目录存在计算机上不在项目中,不同的系统在的位置不一样。using UnityEngine; using System.IO; using System; using UnityEditor; public class demo1 : MonoBehaviour{ //可读不可写 string streamingAssetsTxt = string.Empty; //可读可写 string m_PersistentDataTxt = string.Empty; void Start(){ string mystr1 = Resources.Load<TextAsset>("demo1").text; Debug.Log(mystr1); streamingAssetsTxt = File.ReadAllText(Path.Combine(Application.streamingAssetsPath, "test.txt")); print(streamingAssetsTxt); } /** * 打开 PersistentDataPath的目录 */ [MenuItem("Assets/Open PersistentDataPath")] static void Open() { EditorUtility.RevealInFinder(Application.persistentDataPath); } void OnGUI() { if(GUILayout.Button("<size=50>写入并读取时间</size>")) { string path = Path.Combine(Application.persistentDataPath, "test.txt"); File.WriteAllText(path,DateTime.Now.ToString()); m_PersistentDataTxt = File.ReadAllText(path); print(m_PersistentDataTxt); } } }
using System.Collections.Generic; using UnityEngine; using System.IO; using System; using System.Text; public class RecordUtil { //游戏存档保存的根目录 static string RecordRootPath { get { #if(UNITY_EDITOR || UNITY_STANDALONE) return Application.dataPath + "/../Record/"; #else return Application.persistentDataPath + "/Record/"; #endif } } //游戏存档 static Dictionary<string, string> recordDic = new Dictionary<string, string>(); //标记某个游戏存档是否需要重新写入 static List<string> recordDirty = new List<string>(); //标记某个游戏存档是否需要删除 static List<string> deleteDirty = new List<string>(); //表示某个游戏存档读取时需要重新从文件中读取 static List<string> readDirty = new List<string>(); static private readonly UTF8Encoding UTF8 = new UTF8Encoding(false); static RecordUtil() { readDirty.Clear(); if (Directory.Exists(RecordRootPath)) { foreach (string file in Directory.GetFiles(RecordRootPath, "*.record", SearchOption.TopDirectoryOnly)) { string name = Path.GetFileNameWithoutExtension(file); if (!readDirty.Contains(name)) { readDirty.Add(name); Get(name); } } } } //强制写入文件 public static void Save() { Debug.Log("save"); foreach (string key in deleteDirty) { try { string path = Path.Combine(RecordRootPath, key + ".record"); if (recordDirty.Contains(key)) { recordDirty.Remove(key); } if (File.Exists(path)) { File.Delete(path); } } catch (Exception ex) { Debug.LogError(ex.Message); } } deleteDirty.Clear(); foreach (string key in recordDirty) { string value; if (recordDic.TryGetValue(key, out value)) { if (!readDirty.Contains(key)) { readDirty.Add(key); } string path = Path.Combine(RecordRootPath, key + ".record"); recordDic[key] = value; try { Directory.CreateDirectory(Path.GetDirectoryName(path)); File.WriteAllText(path, value, UTF8); } catch (Exception ex) { Debug.LogError(ex.Message); } } } recordDirty.Clear(); } public static void Set(string key, string value) { recordDic[key] = value; if (!recordDirty.Contains(key)) { recordDirty.Add(key); } #if UNITY_EDITOR || UNITY_STANDALONE Save(); #endif } public static string Get(string key) { return Get(key, string.Empty); } public static string Get(string key, string defaultValue) { if (readDirty.Contains(key)) { string path = Path.Combine(RecordRootPath, key + ".record"); try { string readStr = File.ReadAllText(path, UTF8); recordDic[key] = readStr; } catch (Exception ex) { Debug.LogError(ex.Message); } readDirty.Remove(key); } string value; if (recordDic.TryGetValue(key, out value)) { return value; } else { return defaultValue; } } public static void Delete(string key) { if (recordDic.ContainsKey(key)) { recordDic.Remove(key); } if (!deleteDirty.Contains(key)) { deleteDirty.Add(key); } #if UNITY_EDITOR || UNITY_STANDALONE Save(); #endif } }
测试使用
using UnityEngine; using UnityEditor; public class Test1 : MonoBehaviour { void Start() { Setting setting = new Setting(); setting.stringValue = "测试字符串"; setting.intValue = 10000; RecordUtil.Set("setting", JsonUtility.ToJson(setting)); } private Setting m_Setting = null; void OnGUI() { if (GUILayout.Button("<size=50>获取存档</size>")) { m_Setting = JsonUtility.FromJson<Setting>(RecordUtil.Get("setting")); } if (m_Setting != null) { GUILayout.Label(string.Format("<size=50> {0},{1} </size>", m_Setting.intValue, m_Setting.stringValue)); } } void OnApplicationPause(bool pauseStatus) { //当游戏即将进入后台时,保存存档 if (pauseStatus) { RecordUtil.Save(); } } [System.Serializable] class Setting { public string stringValue; public int intValue; } }
操作XML时,需要用到 System.Xml
命名空间。
模拟真实事件的物体碰撞,跌落,使用了 nvida 的physX。新建cube,add component,选择 Physics给cube添加一个刚体,
Rigidbody
刚体表示受力的作用,Collider
表示使用碰撞检测。
using UnityEngine; namespace Scene4 { public class RigidControl : MonoBehaviour { private Rigidbody obj; private void Start() { obj = GetComponent<Rigidbody>(); } private void Update() { if (Input.GetKeyDown(KeyCode.A)) { // 给物体添加一个右上方的力, Impulse是冲击力,瞬间爆发 obj.AddForce(new Vector3(1, 1, 0) * 10, ForceMode.Impulse); } if (Input.GetKeyDown(KeyCode.Q)) { // 给物体添加一个右上方的力, 牵引力,不停的按下才可以 obj.AddForce(new Vector3(1, 1, 0) * 100, ForceMode.Acceleration); } if (Input.GetKeyDown(KeyCode.W)) { // 直接给物体一个速度 obj.velocity = new Vector3(1, 1, 0) * 3; } } } }
碰撞检测
地板Plane都有默认的 Mesh Collider
,发生碰撞的条件是两个物体之间都有Collider并且至少一方有刚体。Box Collider
也有材质但是和MeshRenderer
的材质(渲染材质)不一样,这个叫做物理材质(不同的物理属性)。可以使用右键 Physics Material
创建物理材质。
面板中的属性依次是动摩擦系数、静摩擦系数、弹性(物体落下弹起的效果)、摩擦系数合并(比如取最大值)
可以调整碰撞体的大小(不接触物体但是能碰到的效果)。
is Trigger 勾选了就是trigger触发器,没有力的作用(比如小球和地面,勾选了直接就穿过了地面)。
碰撞有2种消息(trigger的和非trigger的),3种状态(Enter、Stay、Exit)。
// 被碰到的物体的名字 private void OnCollisionEnter(Collision c) { print("OnCollisionEnter" + c.gameObject.name); } private void OnCollisionStay() { print("OnCollisionStay"); } private void OnCollisionExit() { print("OnCollisionExit"); } private void OnTriggerEnter() { print("OnTriggerEnter"); }
射线是一个点向另一个点发射一条线,一旦和其他的模型发生碰撞,就停止发射。射线是摄像机发出的。
鼠标点击一个点,摄像机和这个点连线,之后和哪个物体发生了碰撞就是点击了哪个物体。
using UnityEngine; public class RayTest : MonoBehaviour { // 游戏对象 添加组件 LineRenderer private LineRenderer line; private void Start() { line = GetComponent<LineRenderer>(); } private void Update() { if (Input.GetMouseButtonDown(0)) { if (Camera.main) { print(Input.mousePosition); Ray r = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; // 如果和某物体发生了碰撞, 100内,第4个参数是layer,默认所有层,这个层是二进制 1 << 8 代表第8层 if (Physics.Raycast(r, out hit, 100f)) { // 碰撞的点 print(hit.point); // 碰到的物体 print(hit.collider.gameObject); line.enabled = true; line.SetPosition(1, hit.point); // 销毁碰到的物体 Destroy(hit.collider.gameObject); } else { line.enabled = false; print("没碰撞"); } } } } }
主要应用到会动的游戏物体上,不会动的就用刚体就好了,也不需要Collider来碰撞检测了。
前两项是坡度(超过这个度数爬不了)和最小位移,角色控制器默认给加胶囊体。
using UnityEngine; public class MoveMove : MonoBehaviour { private CharacterController controller; private Camera main; private float speed; void Start() { controller = GetComponent<CharacterController>(); speed = 10000f; main = Camera.main; } void Update() { // SimpleMove 有重力的作用,move没有 // controller.SimpleMove() // 这个是世界坐标系 // controller.SimpleMove(Vector3.forward * speed * Time.deltaTime * Input.GetAxisRaw("Horizontal")); // 推荐使用本地坐标系,往自己的前方走 controller.SimpleMove(transform.forward * speed * Time.deltaTime * Input.GetAxisRaw("Horizontal")); } private void LateUpdate() { // 摄像机跟随 // main.transform.position = transform.position + new Vector3(0, 0, -1f); } }
最多32个层,内置0-7改不了,是二进制表示 1 << 8代表第8层。
GameObject Terrain,可以创建一些自定义的山啊什么的,一般不使用,一般使用自定义地形。
点击可以切换是否显示,点箭头可以展开更多选项
导航就是任务行走路线,也可以自动避开一些障碍物。菜单windows -> AI -> navigation打开面板。
创建一个这样的地形,选中所有的物体,然后
给运动的物体,添加一个导航组件
给物体绑定脚本
using UnityEngine; using UnityEngine.AI; public class NavController : MonoBehaviour { private NavMeshAgent a; void Start() { a = GetComponent<NavMeshAgent>(); } void Update() { if (Input.GetMouseButtonDown(0)) { var r = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (Physics.Raycast(r, out hit)) { var p = hit.point; a.SetDestination(p); } } } }
动态障碍物
给中间的门去掉 static navigation,并且添加 Nav Mesh Obstacle
组件
网格链接(就是导航区域可链接起来)
比如可以从高台跳下去
首先,选中可以跳的物体勾选上
然后设置高度(下边选项是跳跃距离),并重新烘焙即可
如果有两个特定地方,需要链接,首先在场景创建两个地点cube。然后添加 Off Mesh Link
组件
编辑器使用的代码应该仅限于编辑模式下,也就是说正式的游戏包不应该包含这些代码。Unity提供了一个规则:如果属于编辑模式下的代码,需要放在Editor文件夹下;如果属于运行时执行的代码,放在任意非Editor文件夹下即可。
如图,在editor目录下新建脚本。
using UnityEngine; using UnityEditor; public class TestMenu : MonoBehaviour { [MenuItem("Assets/My Tools/Tools 1",false,2)] static void MyTools1() { Debug.Log(Selection.activeObject.name); } [MenuItem("Assets/My Tools/Tools 2",false,1)] static void MyTools2() { Debug.Log(Selection.activeObject.name); } }
效果如下
GUI不可视化,做测试比较方便,Unity4.6出现的新的UI技术称之为UGUI。
private void OnGUI() { // 手动布局 GUI GUI.Label(new Rect(10, 10, 100, 50), "halo"); if (GUI.Button(new Rect(10, 30, 100, 50), "Get BTC")) { print("you get 10000 BitCoin"); } input = GUI.TextField(new Rect(10, 111, 100, 50), input); if (input == "666") { print("ok"); } // 自动布局 if (GUILayout.Button("btn1")) { print("btn1"); } }
UI必须作为canvas的子对象存在,创建的时候,自动创建Event System。
using UnityEngine; public class Move1 : MonoBehaviour { private GameObject target; // 选中物体,Edit-> lock view to selected就可以运行游戏时,随着物体的视角 private void Start() { // 建议unity一个固定的帧率,不一定有效 Application.targetFrameRate = 60; target = GameObject.Find("Cube1"); transform.LookAt(target.transform); print($"初始距离是{getDistance()}"); } float getDistance() { return (transform.position - target.transform.position).magnitude; } void Update() { if (getDistance() < 0.1f) { print("已经到达目标"); }else { print("moving"); this.transform.Translate(0,0, 1.2f * Time.deltaTime); } } }
int a = 0;
while(a < -1){
//我们将obj1初始化为一个Cube立方体,当然我们也可以初始化为其他的形状
GameObject obj1 = GameObject.CreatePrimitive(PrimitiveType.Cube);
//设置物体的位置Vector3三个参数分别代表x,y,z的坐标数
obj1.transform.position = new Vector3((float)(1 + a * 1.3),1,1);
//给这个创建出来的对象起个名字
obj1.name = ("dujia" + a);
a++;
}
GameObject gb = GameObject.FindWithTag("Enemy");
print(gb.name);
print(gb.GetComponent<Enemy>().money);
Enemy .cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Enemy : MonoBehaviour
{
public int money = 100;
}
新建两个 Enemy0和Enemy1的游戏对象,并绑定上述脚本
public class GameObject1 : MonoBehaviour { void OnGUI() { if (GUILayout.Button("GameObject1")) { // 单个敌人 GameObject gb = GameObject.FindWithTag("Enemy"); print(gb.name) print(gb.GetComponent<Enemy>().money); // 如果有多个 GameObject[] gbs = GameObject.FindGameObjectsWithTag("Enemy"); for (int i = 0; i < gbs.Length; i++) { print(gbs[i].GetComponent<Enemy>().money); } Enemy[] all = Object.FindObjectsOfType<Enemy>(); for (int i = 0; i < all.Length; i++) { if(all[i].money < 60){ all[i].GetComponent<MeshRenderer>().material.color = Color.red; } } } } }
public class TransFormHelper { // 层级未知查找子物体 public static Transform Getchild(Transform parent, string kidName) { Transform childTransForm = parent.Find(kidName); if (childTransForm != null) { return childTransForm; } int count = parent.childCount; for (int i = 0; i < count; i++) { Transform ct = Getchild(parent.GetChild(i), kidName); if (ct != null) { return ct; } } return null; } } // 使用 Transform tf = TransFormHelper.Getchild(this.transform, "trans1"); tf.GetComponent<MeshRenderer>().material.color = Color.red; // 判断两个物体的距离 print(Vector3.Distance(this.transform.position, tf.position));
在Hierachy面板,右键UI选择Text,新建文本。注意,TextMeshPro
是 Unity 的最终文本解决方案。它是 Unity UI Text 和旧版 Text Mesh 的完美替代方案。
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Counter : MonoBehaviour{ public int second = 120; private float nextTime = 1; private float totalTime = 0; private TMPro.TextMeshProUGUI text1; void Start(){ this.text1 = this.GetComponent<TMPro.TextMeshProUGUI>(); // 从1s开始重复调用,间隔也是1s InvokeRepeating("TimerFn3", 1, 1); // 3s后执行 // Invoke("TimerFn3", 3); } void TimerFn3() { if (second > 0){ second--; var res = string.Format("{0:d2}:{1:d2}", second / 60, second % 60); this.text1.text = res; if (second < 10){ this.text1.color = Color.red; } }else { CancelInvoke("TimerFn3") } } // 适合发射子弹 void TimerFn() { if (Time.time > nextTime) { if (second > 0){ second--; var res = string.Format("{0:d2}:{1:d2}", second / 60, second % 60); this.text1.text = res; nextTime = Time.time + 1; if (second < 10){ this.text1.color = Color.red; } } } } void TimerFn2(){ totalTime += Time.deltaTime; if (totalTime >= 1) { totalTime = 0; if (second > 0){ second--; var res = string.Format("{0:d2}:{1:d2}", second / 60, second % 60); this.text1.text = res; if (second < 10){ this.text1.color = Color.red; } } } } void Update(){ // this.TimerFn2(); } }
新建9个箱子,1个子弹,都做成预制体
新建一个空的游戏对象,加下面的脚本
using UnityEngine; public class HitBox : MonoBehaviour { // 定义为public,可以在软件中看到,并且可以直接预制体拖过去就可以 public GameObject Box; public GameObject Bullet; public Texture2D pointer; private float FireTime1, FireTime2; private void Start() { for (int i = 0; i < 5; i++) { for (int j = 0; j < 5; j++) { var box = Instantiate(Box); box.transform.position = new Vector3(-2+i, 0.5f+j, 4.5f); } } FireTime1 = 0.5f; FireTime2 = 0; Cursor.SetCursor(pointer, new Vector2(pointer.width / 2,pointer.height / 2), CursorMode.Auto); } private void Update() { if (Input.GetButton("Fire1") && Camera.main) { FireTime2 += Time.deltaTime; if (FireTime2 >= FireTime1) { FireTime2 = 0; var bullet = Instantiate(Bullet); bullet.transform.position = Camera.main.transform.position; var r = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; // 这样会出来很多小球 if (Physics.Raycast(r, out hit, 50)) { bullet.GetComponent<Rigidbody>().velocity = (hit.point - bullet.transform.position) * 10; } else { print("没碰撞"); } } } } }
当子弹和箱子不可见时,需要销毁,所以给它们附加如下的脚本
using UnityEngine;
public class DistroyOBj : MonoBehaviour
{
private void OnBecameInvisible() {
Destroy(this.gameObject);
}
}
可以设置摄像机的远方距离,做一下优化
下载一个瞄准的图片素材,变成cursor
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。