导读:2.3 窗口布局.Unity默认窗口布局.Hierarchy 层级窗口.Scene 场景窗口,3D视图窗口.Game 游戏播放窗口.Inspector 检查器窗口,属性窗口.Project 项目窗口.Console 控制台窗口.恢复默认布局 Window | Layouts |
Unity默认窗口布局
恢复默认布局 Window | Layouts | Default
调大页面字体 Preference | UI Scaling
新项目默认创建了 SampleScene 场景 {摄像机,平行光}
SampleScene 里 {摄像机,平行光} 就是两个游戏物体
添加物体
选中物体(橙色轮廓)(Inspector显示该物体组件属性)
重命名、删除物体
移动物体
左手坐标系,当x轴向右,y轴向上,z轴向里
视图旋转默认按视图中心点旋转
可以在 Inspector 窗口拖动 X Y Z
操作模式
更多操作
主要涉及单c#文件插件(切换视图快捷功能)的安装与使用
Mesh,存储了模型的形状数据
Unity中观察模型网格(场景窗口右侧栏,2D按钮左边)
高模 :面数越多,物体表面越精细,GPU负担也就越重
mesh filter 组件定义网格数据
Material 定义物体的表面细节 (颜色,金属,透明,凹陷,突起)
创建、使用材质
mesh renderer 组件负责渲染,使用材质相当于修改该组件的 Materials 字段,可直接拖动材质到该字段或打开材质浏览器 。 (检查器窗口右上角可锁定)
用一张图定义物体的表面颜色。 模型每个面有不同颜色,与贴图映射,在建模软件里完成
将贴图拖动至 albedo,可以看到叠加的效果(反射率改为白色),按 BackSpace 清掉贴图
建模师提供的模型,本身已经带了网格、表面材质、材质贴图
FBX模型一般包含 mesh(定义形状),material(定义光学特性),texture(定义表面像素颜色),有的模型可能定义多个材质。将FBX模型拖动至窗口中生成对象(FBX本身也是一种预制体 )
贴图文件的路径是约定好的,与fbx相同目录,或者同级 Textures 目录
复制资源 CTRL + D
除此之外,可将选择的文件导出成资源包 .unitypackage ,导出时可把依赖文件一并导出。再通过 .unitypackage 导入 (整个Assets目录也可以导出)
Pivot 物体操纵基准点 ,可以在任意位置,轴心点是在建模软件中指定的,可以用空物体当父节点修改原轴心
Center 几何中心点,一个物体绕中心点旋转(炮塔例子)。多个物体则是物体合体之后的中心点
在 Hierarchy 窗口呈现两个物体之间的关系(拖物体B到物体A下)
相对坐标:子物体坐标相对于父物体(子物体坐标等于相对坐标+父节点坐标)
物体节点可绑定多个组件(component ),一个组件代表一个功能
如 Mesh Filter 网格过滤器(加载Mesh);Mesh Renderer 网格渲染器(渲染Mesh)
Transform 所有物体都有、不能被删除(基础组件)
脚本组件,游戏驱动逻辑,类名和文件名需要一致 。编译过程是自动的
只有挂载脚本才能被调用:物体节点添加组件 、拖动到检查器窗口下面
脚本类继承自 MonoBehaviour
GameObject obj = this.gameObject;
string objName = obj.name;
Debug.Log(objName);
Transform tr = this.transform; // this.gameObject.transform
Vector3 pos = tr.position;
Debug.Log(pos);
一般常使用 localPosition ,与检查器中的值一致
修改本地坐标
this.transform.localPosition = new Vector3(0f, 0f, 3.5f);
建议先看 10.1 帧更新
不使用 Time.deltaTime 来移动物体是不匀速的(因为时间增量不同)
正确移动方法是 速度 * 时间 (每秒走固定米数,每帧移动距离不同)
void Update()
{
Vector3 pos = this.transform.localPosition;
pos.z += Time.deltaTime * 10f;
this.transform.localPosition = pos;
}
Update(帧更新):每帧调用一次
Unity 不支持固定帧率,但可以设置一个近似 帧率 Application.targetFrameRate = 60;
使用 transform.Translate(dx,dy,dz) 实现相对运动(参数是坐标增量)
可以添加第四个参数即 transform.Translate(dx,dy,dz,space)
GameObject.Find(“Sphere”) 根据名字、路径查找物体
this.transform.LookAt(flag.transform) 将物体 Z 轴转向某一位置,然后每帧沿着 forward 方向按 2m/s 速度前进
void Start()
{
GameObject flag = GameObject.Find("Sphere");
this.transform.LookAt(flag.transform);
}
void Update()
{
float speed = 2f;
float distance = speed * Time.deltaTime;
this.transform.Translate(0,0,distance,Space.Self);
}
Vector3 的 magnitude 属性表示向量长度
Vector3 p1 = this.transform.position;
Vector3 p2 = flag.transform.position;
Vector3 p = p2 - p1;
float distance = p.magnitude;
物体移动到另一物体停止移动
private GameObject flag;
void Start()
{
flag = GameObject.Find("Sphere");
this.transform.LookAt(flag.transform);
}
void Update()
{
Vector3 p1 = this.transform.position;
Vector3 p2 = flag.transform.position;
Vector3 p = p2 - p1;
float distance = p.magnitude;
if (distance > 0.3f)
{
float speed = 2f;
float dis = speed * Time.deltaTime;
this.transform.Translate(0,0,dis,Space.Self);
}
}
选择物体,Edit | Lock View to Selected (SHIFT + F)
Quaternion 四元组
transform.rotation 不便操作,不建议使用
Euler Angle 欧拉角
transform.eulerAngles
transform.LocalEulerAngles
this.transform.localEulerAngles = new Vector3(0, 30, 0);
Vector3 angles = this.transform.localEulerAngles; angles.y += 30 * Time.deltaTime; this.transform.localEulerAngles = angles;
⭐ transform.Rotate(dx,dy,dz,space) 与 Translate 使用方式类似
⭐ 实现公转:父物体带动子物体旋转
场景加载过程(框架自动完成)
属于 MonoBehaviour (统一行为特性类)的消息函数(事件函数、回调函数)
已被禁用的物体 Start / Update 不会被调用,Awake / Start 方法只会被执行一次
🍔 Awake 总被调用是根据当前脚本组件的启用、禁用状态来说的,而不是根据整个物体节点的生效、不生效状态,物体节点如果不生效 Awake 不会被调用
另外,如果脚本组件只有一个 Awake 方法,那么脚本组件的启用、禁用也就没有意义,Unity不为它添加勾选框
脚本执行顺序与层级摆放顺序无关系,默认所有脚本的执行优先级为 0
一个空节点挂载游戏全局设置的脚本(高优先级)
公有类成员变量:让开发者自定义参数从而控制脚本组件功能
添加特性,为参数添加编辑器提示
[Tooltip("这个是Y轴的角速度")]
检查器参数、Awake、Start 都对某一参数初始化时的调用顺序
个人认为可以在 Awake、Start 对参数进行验证操作
基类类型、Vector3、Color 都是结构体值类型
值类型特点
节点、组件、资源、数组类型
public GameObject flag;
在帧更新方法添加,前两方法针对一次鼠标事件只会True一次(成对关系) ,第三个方法多次True
两个鼠标事件是全局的,脚本之间互不影响
获取屏幕长宽
private void Start()
{
int width = Screen.width;
int height = Screen.height;
Debug.Log($"{width} , {height}");
}
获取屏幕坐标
Input.mousePosition 仅X、Y有值,屏幕左下角为原点 ,单位为像素
if (Input.GetMouseButtonDown(0))
{
Vector3 mousePos = Input.mousePosition;
Debug.Log(mousePos);
}
物体世界坐标转屏幕坐标
用于判断物体是否超出屏幕范围(出界是物体轴心点出界,是能看到物体剩余部分的 )
Camera.main.WorldToScreenPoint(pos)
X,Y是物体在屏幕的哪个位置,Z是物体距离摄像机的距离
Vector3 pos = this.transform.position;
Vector3 screenPos = Camera.main.WorldToScreenPoint(pos);
if (screenPos.x < 0 || screenPos.x > Screen.width) // 左右边
{
Debug.Log("出界了");
}
与鼠标输入类似
将代码组件与音乐组件放至同节点(顺序无影响) ;this.GetComponent
void Update()
{
if (Input.GetMouseButtonDown(0))
PlayMusic();
}
void PlayMusic()
{
AudioSource audio = this.GetComponent<AudioSource>();
if (audio.isPlaying)
audio.Stop();
else audio.Play();
}
情景:用主控脚本 控制背景音乐空节点 的音乐组件 的播放
不常用方法: 在检查器设置节点引用,脚本通过物体节点获得组件
public GameObject bgmNode;
void Update()
{
if (Input.GetMouseButtonDown(0))
PlayMusic();
}
void PlayMusic()
{
AudioSource audio = bgmNode.GetComponent<AudioSource>();
if (audio.isPlaying)
audio.Stop();
else audio.Play();
}
常用方法:在检查器里设置组件引用,脚本直接访问该组件
public AudioSource bgmComponent;
void Update()
{
if (Input.GetMouseButtonDown(0))
PlayMusic();
}
void PlayMusic()
{
AudioSource audio = bgmComponent;
if (audio.isPlaying)
audio.Stop();
else audio.Play();
}
个人理解每个组件类都有 GetComponent
情景:用一个脚本组件 控制另一个脚本组件 的公开字段 ,如修改转速
可以是API获取,通过物体节点再获取脚本组件类型,也可以直接引用,下面是直接引用做法(Unity框架自动完成组件查找过程)
public class InputLogic : MonoBehaviour
{
public FanLogic fan;
void Update()
{
if (Input.GetMouseButtonDown(0))
{
fan.rotateY = 90f;
}
}
}
public class FanLogic : MonoBehaviour
{
public float rotateY;
void Update()
{
this.transform.Rotate(0,rotateY * Time.deltaTime,0,Space.Self);
}
}
该方法利用反射机制,效率低,不常用,用于调用其他物体中组件的方法
执行过程
找到节点绑定的所有组件
在所有组件下寻找方法名对应的方法,找到就执行,找不到就报错
public class FanLogic : MonoBehaviour { public float rotateY; void Update() { this.transform.Rotate(0,rotateY * Time.deltaTime,0,Space.Self); }
void DoRotate()
{
rotateY = 90f;
}
}
public class InputLogic : MonoBehaviour { public GameObject fanNode; void Update() { if (Input.GetMouseButtonDown(0)) { fanNode.SendMessage(“DoRotate”); } } }
逻辑很简单,主控节点引用两个脚本组件,然后根据输入修改这两个脚本组件的状态;代码中通过调用各个组件的公开实例方法来修改字段成员
public class RotateLogic : MonoBehaviour
{
float m_rotateSpeed;
void Update() =>
this.transform.Rotate(0, m_rotateSpeed * Time.deltaTime, 0, Space.Self);
public void DoRotate() => m_rotateSpeed = 360*3;
public void DoStop() => m_rotateSpeed = 0;
}
public class FlyLogic : MonoBehaviour
{
float m_speed = 0;
void Update()
{
float height = this.transform.position.y;
float dy = m_speed * Time.deltaTime;
if( dy > 0 && height < 4 )
this.transform.Translate(0, dy, 0, Space.Self);
else if ( dy < 0 && height > 0)
this.transform.Translate(0, dy, 0, Space.Self);
}
public void Fly ()=> m_speed = 1;
public void Land() => m_speed = -1;
}
public class MainLogic : MonoBehaviour
{
public RotateLogic rotateLogic;
public FlyLogic flyLogic;
void Start()
{
Application.targetFrameRate = 60;
rotateLogic.DoRotate();
}
void Update()
{
if(Input.GetKey(KeyCode.W))
flyLogic.Fly();
else if (Input.GetKey(KeyCode.S))
flyLogic.Land();
}
}
效率低,不适应变化;如果有父节点最好指定一下路径;不存在返回null;不常用,通常用公开字段引用对象的方法 ;查找的是生效节点,不生效节点不纳入查找范围
void Start()
{
GameObject node = GameObject.Find("无人机/旋翼");
RotateLogic rl = node.GetComponent<RotateLogic>();
rl.DoRotate();
}
🍔 如果在最前面加/
表示从根目录开始查找
父子级关系由 Transform 维持
获取父级Transform,
通过父级Transform获取父级GameObject
打印父节点名字
void Start() { Transform parent = this.transform.parent; GameObject parentNode = parent.gameObject; Debug.Log(parentNode.name); // 等同 transform.name }
transform 实现了迭代器接口可以被 foreach 遍历,拿到多个子节点
void Start()
{
foreach (Transform child in transform)
{
Debug.Log(child.name);
}
}
也可通过 getChild(int) 索引获取,下标从 0 开始。下面获取第二个子节点transform,不存在返回null
Debug.Log(transform.GetChild(2).name);
Debug.Log(transform.GetChild(2) is Transform);
用法与名称查找节点类似;不存在返回null;与名称查找节点不同的是子级节点不生效,transform也能被查找到
void Start()
{
Transform t = transform.Find("旋翼 (1)/旋翼 (2)");
if (t is null) Debug.Log("Nothing found");
else
Debug.Log(t.name);
}
transform组件实例方法 SetParent(node) 设置当前transform的父级,如果传入null则无父节点(根目录节点)
void Start()
{
Transform node = transform.Find("/aa");
transform.SetParent(node);
}
生效与不生效;也相当于显示与隐藏;个人理解为(启用当前全部组件,禁用当前全部组件)
GameObject 类型实例的实例方法 SetActive ;修改自身节点是否生效
void Start()
{
var obj = transform.gameObject;
Debug.Log(obj.activeSelf);
obj.SetActive(false);
Debug.Log(obj.activeSelf);
}
当前节点修改其他节点是否生效
private GameObject obj;
private void Start() =>
obj = GameObject.Find("aa");
void Update()
{
if (Input.GetMouseButtonDown(0))
{
Debug.Log(obj.activeSelf);
obj.SetActive(!obj.activeSelf);
Debug.Log(obj.activeSelf);
}
}
修改子节点是否生效
void Update()
{
if (Input.GetMouseButtonDown(0))
{
GameObject obj = transform.Find("dd").gameObject;
Debug.Log(obj.activeSelf);
obj.SetActive(!obj.activeSelf);
Debug.Log(obj.activeSelf);
}
}
private int index = 0;
private void Start()
{
foreach (Transform child in transform)
child.gameObject.SetActive(false);
transform.GetChild(index).gameObject.SetActive(true);
}
private void Update()
{
if (Input.GetKeyDown(KeyCode.Space))ChangeShape();
}
private void ChangeShape()
{
transform.GetChild(index).gameObject.SetActive(false);
index = (index + 1) % transform.childCount;
transform.GetChild(index).gameObject.SetActive(true);
}
情景:挂一个AudioSource(音频播放)组件,不指定音频AudioClip(音频容器类);要求利用脚本指定播放的资源
注意:在使用 PlayOneShot 实例方法的情况下,音频播放组件的 clip 字段并没有被设定
public AudioClip bgm;
private void Start()
{
var audioSource = GetComponent<AudioSource>();
// audioSource.clip = bgm;
// audioSource.Play();
audioSource.PlayOneShot(bgm);
}
情景:挂一个AudioSource组件,不指定音频AudioClip;要求利用脚本随机播放几首歌曲里的一首;
public AudioClip[] bgms;
private void Start()
{
if (bgms.Length == 0)
Debug.Log("我歌呢");
var audioSource = GetComponent<AudioSource>();
// audioSource.PlayOneShot(bgms[Random.Range(0,bgms.Length)]);
audioSource.clip = bgms[Random.Range(0, bgms.Length)];
audioSource.Play();
Debug.Log(audioSource.clip.name);
}
直接修改颜色也可以直接修改Albedo的颜色(超出入门范畴)
public Material[] ms;
private int index = 0;
private void Update()
{
if (Input.GetMouseButtonDown(0))
{
index = (index + 1) % ms.Length;
Material m = ms[index];
MeshRenderer mr = GetComponent<MeshRenderer>();
mr.material = m;
}
}
继承了 MonoBehaviour;利用了反射;delay、interval 是秒不是毫秒
注意:每次 InvokeRepeating,都会添加一个新的调度(加到调度队列),然后大循环每次循环遍历调度队列
private void Start()
{
// Invoke("DoSomething",1);
InvokeRepeating("DoSomething",1,2);
}
void DoSomething() =>
Debug.Log("HELLO WORLD " + Time.time);
Unity 引擎核心是单线程的
private void Start()
{
Debug.Log(Thread.CurrentThread.ManagedThreadId);
InvokeRepeating("DoSomething",1,2);
}
private void Update()
{
Debug.Log(Thread.CurrentThread.ManagedThreadId);
}
void DoSomething() =>
Debug.Log(Thread.CurrentThread.ManagedThreadId);
调用一次 CancelInvoke(string func) 终止了两个同函数的调度;全部都取消不用加参数
private static int COUNT = 1;
private void Start()
{
InvokeRepeating("DoSomething",1,2);
InvokeRepeating("DoSomething",1,2);
}
private void Update()
{
if (Time.time > 10)
if (IsInvoking("DoSomething"))
{
CancelInvoke("DoSomething");
Debug.Log("调度被终止了");
}
}
void DoSomething()
{
int i = COUNT++;
Debug.Log($"我是任务 {i}");
}
public Material[] colors;
private int index = 0;
private void Start()
{
Invoke("ChangeColor",0);
}
void ChangeColor()
{
GetComponent<MeshRenderer>().material = colors[index];
index = (index + 1) % colors.Length;
if (index == 1)
Invoke("ChangeColor", 3);
else if(index == 2)
Invoke("ChangeColor", 0.5f);
else
Invoke("ChangeColor", 3);
}
public float speedY = 0f;
private int control = 1;
private void Start()
{
InvokeRepeating("SpeedControl",0,0.1f);
}
void Update()
{
transform.Rotate(0,speedY * Time.deltaTime,0,Space.Self);
if (Input.GetMouseButtonDown(0))
control = -control;
}
void SpeedControl()
{
speedY = speedY + 10f * control;
speedY = speedY > 180 ? 180 : speedY;
speedY = speedY < 0 ? 0 : speedY;
}
Vector3 是结构体,里面有三个字段 x,y,z,向量是有方向的量,有长度
v.magnitude 长度(模长)
v.normalized 单位向量标准化(长度为1的向量是单位向量)
常用静态常量有很多
向量有加减运算(初中知识)
向量有乘法运算
不可空值类型不能被设置为 null,可以留空不写,即默认值 0,0,0
public Vector3 speed; // = null; EXCEPTION
物体之间的距离,确切的说是轴心点 之间的距离
向量相减然后求距离
或直接用 Vector3.Distance(Vector3 a,Vector3 b)
public GameObject a; public GameObject b; private void Start() { Vector3 apos = a.transform.position,bpos = b.transform.position; float disc = Vector3.Distance(apos, bpos); Debug.Log(disc); Debug.Log((apos-bpos).magnitude); }
让一个物体沿着某一方向运动(Translate 有多个函数重载方法)
public Vector3 speed;
private void Update()
{
transform.Translate(speed * Time.deltaTime,Space.Self);
}
创建
预先制作好的物体节点(模板),*.prefab
样本节点做好后,拖到资源目录下,会生成预制体文件。原始样本节点可以删除
prefab 文件只记录了节点的信息;不包含材质、贴图数据,仅包含引用(导出时会将依赖一并导出)
实例
编辑
UnityEngine.Object.Instantiate(Object perfab,Transform parent) 有多个重载版本
创建预制体实例后,应做初始化
public GameObject bulletPrefeb;
public GameObject bulletFolder;
public GameObject Canno;
private void Update()
{
if (Input.GetMouseButtonDown(0))
{
GameObject obj = Instantiate(bulletPrefeb, null);
obj.transform.SetParent(bulletFolder.transform);
obj.transform.position = transform.position;
obj.transform.eulerAngles = Canno.transform.eulerAngles;
// obj.transform.rotation = Canno.transform.rotation;
obj.GetComponent<BulletLogic>().speed = 0.5f;
}
}
比如 22.1 的子弹案例
UnityEngine.Object.Destroy(GameObject obj)
public class BulletLogic : MonoBehaviour
{
public float speed;
public float maxDistance;
void Start()
{
Debug.Log("Start Start");
float lifetime = 1;
if (speed > 0) lifetime = maxDistance / speed;
Destroy(gameObject,lifetime);
Debug.Log("Start Finish");
}
void Update() =>
this.transform.Translate(0, 0, speed, Space.Self);
}
public class SimpleLogic : MonoBehaviour
{
public GameObject bulletPrefeb;
public GameObject bulletFolder;
public GameObject Canno;
public float speed = 0.5f;
public float flyTime = 2f;
private void Update()
{
if (Input.GetMouseButtonDown(0))
{
GameObject obj = Instantiate(bulletPrefeb, null);
Debug.Log("Instantiate Start");
obj.transform.SetParent(bulletFolder.transform);
obj.transform.position = transform.position;
obj.transform.eulerAngles = Canno.transform.eulerAngles;
// obj.transform.rotation = Canno.transform.rotation;
obj.GetComponent<BulletLogic>().speed = speed;
obj.GetComponent<BulletLogic>().maxDistance = speed * flyTime;
Debug.Log("Instantiate Finish");
}
}
官方建议不要获取对象欧拉角再覆盖欧拉角,涉及转换的一些问题
而是固定用一个Vector3当做对象的欧拉角
private Vector3 _eulerAngles;
public float rotateSpeed = 30f;
public GameObject Canno;
void Update()
{
float delta = rotateSpeed * Time.deltaTime;
if (Input.GetKey(KeyCode.W))
if (_eulerAngles.x > -60)
_eulerAngles.x -= delta;
if (Input.GetKey(KeyCode.S))
if (_eulerAngles.x < 30)
_eulerAngles.x += delta;
if (Input.GetKey(KeyCode.A))
_eulerAngles.y -= delta;
if (Input.GetKey(KeyCode.D))
_eulerAngles.y += delta;
Canno.transform.localEulerAngles = _eulerAngles;
}
刚体 RigidBody
使物体具有物理学特性。添加刚体组件后由物理引擎负责刚体的运动
碰撞体组件 Collider
设置物体的碰撞体积范围。也由物理引擎负责
默认添加的碰撞体一般情况下 会根据网格自动设置尺寸,可以另外编辑
通过物理材质 ,设置一些参数后将该物理材质赋给碰撞体组件的物理材质引用
RigidBody 组件参数 Is Kinematic 打勾,此时为运动学刚体
⭐零质量,不会受重力影响,但可以设置速度来移动;这种运动刚体完全由脚本控制
需满足以下两个条件
物体是运动刚体
碰撞体开启了 Is Trigger
物理引擎只负责探测(Trigger),不会阻止物体或者反弹
物理引擎计算的是 Collider 之间的碰撞,和物体自身形状无关
当检测到碰撞时,调用当前节点多个事件消息函数,如 OnTriggerEnter
public Vector3 speed; void Update() => transform.Translate(speed * Time.deltaTime,Space.Self);
private void OnTriggerEnter(Collider other) { Debug.Log(“发生碰撞”); Debug.Log(other.name); }
给子弹添加如下代码
private void OnTriggerEnter(Collider other)
{
Debug.Log(other.name);
Destroy(other.gameObject);
Destroy(gameObject);
}
Window | Rendering | Lighting (CTRL + 9)
public class BulletLogic : MonoBehaviour
{
public float speed = 1f;
void Update()
{
transform.Translate(0,0,speed * Time.deltaTime,Space.Self);
}
private void OnTriggerEnter(Collider other)
{
if (!other.name.StartsWith("怪兽")) return;
Destroy(other.gameObject);
Destroy(gameObject);
}
}
public class PlayerLogic : MonoBehaviour
{
public GameObject bulletPrefeb;
public GameObject bulletFolder;
public Transform firePos;
public Transform fireEulerAngles;
public float speed = 15f;
public float lifeTime = 3f;
public float interval = 2f;
private float _interval = 2f;
public float moveSpeed = 15f;
private void Update()
{
_interval += Time.deltaTime;
if (Input.GetMouseButtonDown(0) && _interval > interval)
{
_interval = 0f;
GameObject obj = Instantiate(bulletPrefeb, null);
obj.transform.SetParent(bulletFolder.transform);
obj.transform.position = firePos.position;
obj.transform.eulerAngles = fireEulerAngles.eulerAngles;
obj.GetComponent<BulletLogic>().speed = speed;
obj.GetComponent<BulletLogic>().lifeTime = lifeTime;
}
if(Input.GetKey(KeyCode.A))
transform.Translate(-Time.deltaTime * moveSpeed,0,0,Space.Self);
if(Input.GetKey(KeyCode.D))
transform.Translate(Time.deltaTime * moveSpeed,0,0,Space.Self);
}
}
public class CreatorLogic : MonoBehaviour
{
public GameObject enemyPrefeb;
void Start()
{
InvokeRepeating("CreateEnemy",1f,1f);
}
void CreateEnemy()
{
GameObject obj = Instantiate(enemyPrefeb,transform);
var pos = transform.position;
pos.x += Random.Range(-30, 30);
obj.transform.position = pos;
obj.transform.eulerAngles = new Vector3(0, 180, 0);
}
}
public GameObject explosionPrefeb;
...
private void OnTriggerEnter(Collider other)
{
if (!other.name.StartsWith("怪兽")) return;
Destroy(other.gameObject);
Destroy(gameObject);
GameObject obj = Instantiate(explosionPrefeb, null); // 不要挂载子弹节点下面
// 粒子特效播放完会自毁
obj.transform.position = transform.position;
}
Unity Documentation 、B站 阿发你好 入门视频教程
上一篇:产品代码都给你看了,可别再说不会
下一篇:大佬们 有没有unity游戏开发