故事大概:“我”是一个对中国古代物理十分感兴趣的年轻人,了解到“这个村子”对制作古代器
具有研究,所以“我”来到了“这个村子”学习制作方法。来到村子后,“我”在村口与村长交流说明来意,村长安排我去学习(一段与村长交流的剧情)。// 在村子中“我”收集材料交给村民,再由村民教授“我”制作方法。(村民也会告诉我其中的物理知识,以及背景)制作好的物品最后会变成一张拼图,“我”学会了所有制作方法后,可以去村长家将所有的拼图拼起来,或者会有其他玩法,具体最后根据剧情和道具决定。最后拼好拼图之后,村长会说“恭喜已经完成所有东西的制作.”(类似这样的话)然后通过一系列回忆与升华最后游戏结束。
道具:(具体可见文件资源)一开始暂定先做三个道具试试手;
光影:
- 在模型之中,lod group是一个存在于模型之中的组件,该作用是可以在摄像头拉近物体时使其建模精细,而拉远之后便可以使建模模糊(可以减轻精细建模带来的负担);
- 在给已经铺好的建筑上方添加光源时,首先前提是有一个全局光(本来就自带的),就算嘴上说是全局光但是有些建筑可能照不到全部的光源,所以要复制粘贴一个全局光(俗称补光吧),然后朝着偏一点的方向(自行判断吧)照射(此时在显示器中将灯光改成不会产生阴影的光),所以要在显示器中找到shadow type更改成无即可,这期间可以调整颜色来使画面更加符合心意(暗面的话还可以调整一下intensity(光的强度);
- 在项目中如果有同类的物品可以ctrl把他们都勾选上,然后右键选择创建新的parent然后就可以创建他们的集合体;
- 在模型中有带灯光物品中,要想调整它自身的光照的颜色首先要找到他所在的材质,材质中的emission下面调整光照的颜色以及浓度(都在颜色里面了),要想让这个物品发出光的话要找到这个预制体,点击右边的小三角进去预制体,然后在预制体的物品下创建一个点光源(spot light)在刚才上面几条的基础上调整光源,这些光一般都是有阴影效果的记得加上阴影,处理好这些后要将这个预制体应用一下:点击这个预制体然后在显示器差不多靠上的位置找到overrides选择apply all即可;
- 增加雾效果:点击windows,点击rendering,点击lighting然后就会出现一个界面,点击environment,找到other settings,勾选fog,然后就可以自行调整颜色其余物品了;
- 增加光照氛围感光雾的效果:点击windows,点击package manager,第一行要选择unity自带的相关资料,找到post processing,然后把他装进来,在layer中单独添加一个跟这个文件包一样名字的层,然后将相机的层换成这个,然后在相机中增加组件:post processing layer,并且选择layer为刚刚设置好的,再加一个组件post processing volume,勾选is golobal,然后新建一个文件夹叫post processing,然后在该文件夹中创建一个post processing profile,然后把这个东西拖入到post processing volume组件中的对应位置去,然后选择这个创建出的文件,点击add,添加bloom效果,然后就可以自行调整了;
导航系统网络:
- 跟上一条一样找到windows自带的资源包,找到ai nagivation,然后创建一个ai选择其中的第一个就可以创建出来一个,然后画面中的勾选勾选shownavmash,点击其组件中最后的back,然后呢就会出现蓝色的界面其中不在蓝色区域中的就是障碍物,要是想要让本来在障碍物中的物品变成非障碍物的话,首先选中这个物品,创建一个组件navmeshmolifider中的mode选择第二个即可,然后呢再回去点back应用一下;
- npc的组件中的mesh 碰撞器最开始是没有任何的碰撞效果的,因为在mesh模块中没有东西,所以我们要在npc物体下面创造一个空物体(胶囊体)让这个当一个障碍物,然后也要back应用一下;
- 这里当然我们只用到了最简单的方法,要是想要增加会动的npc障碍物的话还可以再细细研究这个视频,但是时间有限,从现在开始无关内容就不学了;
主角移动:
- 点击主角,增加组件navmeshAgent,然后就可以开始写移动控制脚本;
- 这个脚本呢是根据鼠标点击的位置移动:
using UnityEngine; public class PlayerMove :MonoBehaviour { private NavMeshAgent playerAgent; void Start() { PlayerAgent=GetComponent<NavMeshAgent>();//获取组件 } void Update() { if (Input.GetMouseButtonDown(0))//获得鼠标的位置,在鼠标位置发射一个射线 { Ray ray=Camera.main.ScreenPointToRay(Input.mousePosition);//获取主相机,并且把屏幕上一个点(鼠标位置)转化成射线,此时坐标是屏幕坐标 RaycastHit hit; bool isCollide=Physics.Raycast(ray,out hit);//射线检测(有没有碰撞这个射线) if(isCollide) { playerAgent.SetDestination(hit.point);//获得目标位置即为碰撞的点 } } } }
- 要是觉得加速度等不太好的话,去小组件navmeshagent调整:speed是速度;Angular speed是转速;accieieration是加速度;
- 如果发生碰撞,RaycastHit 会存储与碰撞相关的详细信息,例如点和对象;
- 使用ScreenPointToRay方法将鼠标的位置(屏幕坐标)转换为一条射线。这条射线从主相机的视角出发,穿过鼠标点击的位置,用于检测鼠标点击的3D世界位置。
- 使用Physics.Raycast方法进行射线检测,检测是否发生了碰撞;
- zoomSpeed用于控制鼠标滚轮缩放相机视野的速度;
相机跟随:
- 点击主相机,找到camera组件,找到filed of view通过修改这个值可以实现摄像头对场景的拉近拉远(默认60)
- a首先做相机跟随,在主相机上弄一个脚本,在此之前先把主角标签变成plyer:
using UnityEngine; public class CameraController :MonoBehaviour { public float zoomSpeed=5; private Vector3 offset;//记录相机和主角之间的位置偏移 private Transform playerTransform; void Start() { playerTransform=GameObject.FindGameObjectWithTag("Player").transform; offset=transform.position-playerTransform.position;//获得transform组件来回到主角的位置,相机的位置减去主角的位置得到了偏移的位置 } void Update() { transform.position=playerTransform.position+offset; float scroll=Input.GetAxis("Mouse ScrollWheel");//得到鼠标滑轮的值 Camera.main.fieldOfView +=scroll*zoomSpeed;//这里速度可以自行在游戏中调整 Camera.main.fieldOfView = Mathf.clamp(Camera.main.filedaOfView,37,50);//clamp是用来限定一个值的,限定的这个范围需要自己通过修改filedofview来测试 } }
- transform.position是相机的位置,playerTransform.position是玩家的位置。
- Mathf.Clamp是一个常用的数学方法,用于将一个值限制在指定的范围内;
- 单例(Singleton)是一种设计模式,用于确保某个类在程序运行期间只有一个实例,并提供一个全局访问点来获取这个实例。
交互:
- 首先鼠标点击npc的话就可以发生和npc的对话交互了,但是要与点击地面造成的人物走动区分开所以要先修改对应物体的标签,通过标签来分辨这些点击的物品;
- 可以点击的物品呢大致有:npc,树木,矿井,可以捡起的物品;
- 交互的物品因为交互的形式不一样所以造成的交互类型不一样;
- tag标签我们添加两个一个是ground(地面)第二个是可以交互的物品叫interactable,然后把对应的物品弄上各自的标签,如果npc没有碰撞器点击它不会产生交互作用的话后期需要增加一个碰撞器;
- 然后我们就可以开始修改PlayerMove的代码了:
using UnityEngine; using UnityEngine.AI; using UnityEngine.Eventsystems; public class PlayerMove :MonoBehaviour { private NavMeshAgent playerAgent; void Start() { PlayerAgent=GetComponent<NavMeshAgent>();//获取组件 } void Update() { if (Input.GetMouseButtonDown(0) && EventSystem.current.IsPointerOverGameObject()==false)//获得鼠标的位置,在鼠标位置发射一个射线,并且判断鼠标是否在UI上(current是用来获取这个组件的) { Ray ray=Camera.mian.ScreenPointToRay(Input.mousePosition);//获取主相机,并且把屏幕上一个点(鼠标位置)转化成射线,此时坐标是屏幕坐标 RaycastHit hit; bool isCollide=Physics.Raycast(ray,out hit);//射线检测(有没有碰撞这个射线) if(isCollide) { if(hit.collider.tag=="Ground") { playerAgent.stoppingDistance=0;//这样点击地面不会有那两m的停下距离 playerAgent.SetDestination(hit.point);//获得目标位置即为碰撞的点 }else if(hit.collider.tag=="Interactable") { hit.collider.GetComponent<Interactable>().OnClick(playerAgent);//调用这个类中的这个代码实现移动以及交互,并且把playerAgent传递到这个脚本中 } } } } }
EventSystem.current.IsPointerOverGameObject():检测鼠标是否悬停在UI元素上;
-
这里使用EventSystem.current来获取当前的事件系统组件。
-
在Unity中,事件系统组件(Event System)是一个用于处理输入事件并将其路由到场景中对象的核心系统,它可以管理选中状态:决定哪个游戏对象当前处于选中状态(这里是我们用到的方法);
-
IsPointerOverGameObject() 方法用于检测鼠标是否悬停在任何 UI 元素上
- 修改完之后呢这就是可以创建三个类,交互类是父类,剩下的交互类型分别各自是一个类(创建脚本);
- 被交互的物体上要弄上一个碰撞体根据类型判断比如npc是胶囊碰撞体,注意是跟之前写过的游戏代码不同她不用勾选is trriger;
- 目前先创建了三个脚本:InteractableObject,NPCObject,PickableObject,视频教程中没有的我们应该还要添加一个与树木交互掉落树木的代码;
- InteractableObject脚本内容如下:
this.playerAgent = playerAgent:将传入的玩家导航代理组件赋值给类的成员变量 playerAgent。using UnityEngine; using UnityEngine.AI; public class InteractableObject : MonoBehaviour { private NavMeshAgent playerAgent; private bool haveInteracted=false; public void OnClick(NavMeshAgent playerAgent)//提供一个被点击的方法,括号中是吧主角的组件传递过来 { this.playerAgent=playerAgent;//将传入的玩家导航代理组件赋值给类的成员变量playerAgent playerAgent.stoppingDistance=2;//距离目标位置2m就会停下 playerAgent.SetDestination(transform.position);//设置目标位置移到附近 haveInteracted=false;//每次点击重置这个为没点击的状态 } private void Update() { if(playerAgent!=null && haveInteracted==false && playerAgent.pathPending==false)//一开始值是空的后面的并且是代表路径是否在计算,计算完可行路径,并且它没有交互过她才会走 { if(playerAgent.remainingDistance<=2)//是否到达目标位置 { Interact(); haveInteracted=true; } } } protected virtual void Interact()//交互的方法 { } }
- 在C#中,this关键字用于引用当前类的实例;
- 在C#中,当局部变量(如方法参数)和成员变量同名时,局部变量会隐藏成员变量。如果不加 this,代码会将方法参数赋值给自己,而不是类的成员变量。因此,为了明确指定要操作的是类的成员变量,需要使用 this。
- playerAgent.pathPending == false:确保导航代理的路径计算已完成。
- 补充一个知识点:protected修饰定义:
protected
修饰的成员可以被其所在类及其派生类(子类)访问。特点:-
继承性:允许子类访问父类的某些成员,但外部类无法访问。
-
灵活性:比
private
更灵活,但比public
更安全。使用场景:-
通常用于定义一些需要在子类中被重写或扩展的方法和字段。
-
例如,一个父类的
protected
方法可以在子类中被重写以实现特定的功能
-
-
- 再补充一个知识点:在Unity编程中,
protected virtual void
是一种方法声明方式,结合了protected
和virtual
两个关键字,用于定义一个可以在派生类(子类)中被重写的方法。virtual
是一个关键字,用于声明一个方法是可以被派生类重写的。它允许派生类提供该方法的自定义实现,而不会影响基类的其他部分。 - NPCObject内容如下:
using UnityEngine; public class NPCObject : InteractableObject { public string name; public string[] contentList; public DialogueUI dialogueUI;//用dialouge里面的方法传递过去说过的话 protected override void Interact() { dialogueUI.Show(name,contentList); } }
- PickableObject内容如下:
using UnityEngine; public class PickableObject : InteractableObject { public ItemSO itemSO;//知道是哪个item protected override void Interact() { Destory(this.gameObject); InventoryManager.Instance.AddItem } }
- 场景中显示中文的方法:首先右键创建UI,创建text(用哪个pro就行了),中文显示不出来的原因是因为字体不支持,主要问题出现在font asset中,我们需要一个资源来支持中文字体,所以我们要在资源商店里寻找合适的字体然后导入;
- 当然我们现在补充一点如果是自己弄的字体文件不能直接导入字体模板中,我们需要自己创建一个字体文件,点击windows里面寻找TMP然后点击第一个,然后就会出来单独的一个页面框,第一行是字体源文件,也就是我们自己的一个字体,padding是字符与字符之间的间距,character file(字符文件)最后还需要指定一下(就是你需要哪些字符),将常用的中文做成一个中文文档放在里面即可,character set中要选择最后一个,分辨率最好小一点不然占用空间会很大,分辨率根据最后得到的实际效果看看模糊不具体调整,然后创建,然后保存;(最好的话还是去找素材吧,自己弄哪知道会出来什么千奇百怪的bug)
- 创建npc对话ui时要先创建一个界面image,按住alt键就可以只进行一个左右的放大更好操作对话框的大小,最好材质选择圆角的看起来更好看,image type选择slide就好,然后弄一个文字UI作为NPC的名字放在合适的位置(个人的话还想弄一个半身头照放在左边),这个文本框的设计呢可以之后跟原画商量一下画个有意思的边框什么的,在对话框右下角弄一个按钮(UI中的东西)用来进入下一段对话;
- UI的组件是通过eventsystem这个组件控制的,所以要想控制鼠标点击对话框不会出现行走的情况就要通过控制这个组件的代码来生成;
- 最后将这些制造的UI全部弄进去一个组件,叫做DiaLogueUI,然后为了控制不同的NPc语句要在它上面放置一个代码:
using UnityEngine; public class DialogueUI : MonoBehaviour { public TextMeshProUGUI nameText; public TextMeshProUGUI contentText; public Button continueButton; public List<string> contentList; private int contentIndex=0; private Action OnDialogueEnd; private void Start() { Hide();//隐藏初始的对话框 } public void Show()//展示对话框 { gameObject.SetActive(true); } public void Show(string name,string[] content,Action OnDiagueEnd=null)//函数重载 { nameText.text=name; contentList=new List<string>();//语句可能是空的,确保重新赋值 contentList.AddRange(content);//把数组中的数据拿过来 contentIndex=0; contentText.text=contentList[0]; gameObject.SetActive(true); this.OnDialogueEnd=OnDiagoueEnd; } public void Hide()//隐藏对话框 { gameObject.SetActive(false); } private void OnContinueBottonClick() { contentIndex++; if(contentIndex>=contentList.Count) { OnDialogueEnd?.Invoke(); Hide();return; } contentText.text=contentList[contentIndex];//显示下一段对话 } }
定义:override 用于在子类中重写父类被中声明为 virtual 或 abstract 的方法。
-
public void Show(string name, string[] content)这是一个重载的 Show 方法,用于初始化对话框的内容。
-
contentList = new List<string>():清空之前的对话内容列表。
-
AddRange 方法的作用是将多个元素批量添加到列表的末尾;
- 要将创建的每一个东西都命名,便于后续查找;
- 函数重载知识点补充:在Unity中,函数重载(Function Overloading)是一种面向对象编程中的常见特性,它允许在同一个类中定义多个同名函数,但这些函数的参数列表(参数的类型、数量或顺序)必须不同。函数重载的核心在于函数名相同,但函数的“签名”不同。
- 接下来对话框做好了,做一下点击npc可以弹出对话框的功能:这些功能在npc pbject中实现;
- 每一个public的东西都要有相关的东西写进去,时刻检查;
- 想要制造多个npc先放上去模型,如果自身有碰撞器先去除这个碰撞器,然后给他添加一个nav mesh obatacle组件(因为他自己是一个障碍物),然后再这个组件中选择capusule(第一栏)(说明他是一个障碍物),然后去修改一下障碍物范围(自行修改),然后再加一个胶囊体碰撞器,标签设置成可以交互的物体,然后挂上npc的脚本;
物品管理:
- 物品呢我们有两种吧:消耗品(做任务获得的物品)和收藏品(最后制作成的道具),具体可以见附件;
- 背包里面可以展示的属性有:名称,获取途径(好像就只有这些吧);
- 物品有预制体还有图片(spirit)两种,要掉落捡起来的就是预制体;
- 放脚本的文件下面创建一个文件夹叫做SO用来存放关于物品管理的代码,创建脚本ItemSO,TestScriptObject;(存放物品管理的)
-
然后我们从代码中出来创建在so文件夹下面创建一个第一栏的东西,相对于用代码创造出来的东西,这种东西会更加容易保存数据,然后点击他就可以在里面填写你写的public中的东西,然后就可以保存下来数据了,这个数据文件放在脚本管理文件下不太合适,所以我们要新建一个文件夹,用来存放这些数据DataSO,这个就是scritableobject的作用,直接保存对象的属性,然后哪里想用直接托上去;
-
[CreateAssetMenu]:
这是一个特性,用于在Unity编辑器中添加一个菜单项,方便创建 ItemSO 的实例。它继承自 ScriptableObject,这意味着你可以通过 Unity 编辑器创建和编辑这个类的实例。 -
itemSO 是一个指向 ItemSO 资源的引用。(便捷来说就是可以用数据库里面的东西了)
-
枚举(Enumeration,简称Enum) 是一种数据类型,用于定义一组命名的常量。
-
然后开始写ItemSO的代码:
using UnityEngine; [CreatAssetMenu()] public class ItemSO : ScriptableObject { public string name; public ItemType itemType; public string descriptior;//对他的描述 public Sprite icon;//图标 public GameObject prefab; } public enum ItemType//用来创建上面提到的两种物品类别 { Consumable//消耗品 }
SO 通常指的是 ScriptableObject,这是一种非常强大的功能,用于创建可复用、可持久化的数据容器。
-
作用:itemSO 用于存储与该可拾取对象相关的物品信息。通过这个变量,PickableObject 可以知道当玩家拾取它时,应该给予玩家哪个物品。
-
我们创造好消耗品之类的以后之后,想要统一管理这些数据我们再创建一个脚本控制这些拥有的消耗品,叫做 ItemDBSO:
using System.Collection.Generic; using UnityEngine; [CreateAsssetMenu()] public class ItemDBSO : ScriptableObject { public List<ItemSO> itemList; }
然后就可以从中创建ItemDBSO了,然后我们将它叫做ItemDB,然后点开他的操作台,将所有创造的消耗品和武器拖进这个列表中就可以保存下来了;
-
然后为了更好的管理标签,更好的写标签可以建造一个脚本叫Tag:
public class Tag { public const string PLAYER = "Player"; }
这个脚本的话这次游戏就不用了,运用到标签的所有代码统一写成前面的方式吧,感觉没那么好用啊这个脚本;
-
对于消耗品的爆出,比如砍树时爆出木头,挖矿时爆出矿石,首先将场景建模做好,建模放进去之后,一般情况下我们都不使用mesh collider,比较消耗性能,所以我们将他删除,给他增加box collider;
-
图标的制作:图标直接在场景中截图就比较简单,不用再画了(ctrl A是截图),叫名称的话就教程icon+它自身的名字就行(这样的话东西比较多的时候比较容易找到),或者直接点开预制体找到图标旋转几下截一个图也可以了(这样截图方式其实更好感觉),然后创建文件夹保存所有的贴图叫做sprite,然后将照片导入到这个文件夹中。选中所有的图片,改变一下他们的格式,选择sprite2dUi那个type,mode选择为single,然后直接应用,如果想要看一下这个图片在UI上显示的大小的话,可以在创建UI的那个上面创建一个UI image然后调整自己想要的图标大小,然后将图片拖入,看看多大多小,然后所有的图标最好是高宽一致的,到时候修改一下图片(拖进来之前就修改一下);
-
爆物品(不完全还需要补充):创建一个空物体,是用来管理其他物品的叫做manager吧,然后她身上要携带一个脚本,脚本是用来管理item信息,用来做成单例模式:
using UnityEngine; public class ItemDBManager : MonoBehavior { public static ItemDBManager Instance { get; private set;}//单例模式 public ItemDBSO itemDB;//方便在操作台中更好的控制物品的数据 void Start() { if(Instance!=this && Instance!=null)//检查是否存在单例 { Destroy(this.gameObject);return; } Instance = this;//初始化单例模式 } }
在武器上可以增加一个刚体来控制他的运动,对这串代码的解释,顺便也是对单例模式的一些理解和补充:
Instance
是一个静态的ItemDBManager
对象引用,可以通过ItemDBManager.Instance
全局访问。get
是公共的,允许外部代码获取Instance
的值,Instance:这是一个静态只读属性,用于全局访问 ItemDBManager 的唯一实例。。set
是私有的,只能在ItemDBManager
类内部设置Instance
的值。中间if检测的部分是因为:这种情况通常发生在多个游戏对象上附加了相同的ItemDBManager
脚本;这段代码的作用是实现一个单例模式的物品数据库管理器。通过单例模式,确保整个游戏中只有一个ItemDBManager
实例,避免重复创建。它通过ItemDBSO
类型的变量itemDB
来管理物品数据,具体的物品数据可能存储在ItemDBSO
中,而ItemDBManager
则负责全局访问和管理这些数据。 -
if (Instance != this && Instance != null):
检查是否已经存在一个 ItemDBManager 实例(Instance)。
如果 Instance 已被赋值且当前实例不是 Instance,说明已经存在一个单例。 -
捡起掉落的物品:用pickableobject代码(以前写过的)
-
背包管理:创建一个脚本挂在刚刚创建的空物体manager上面:
using UnityEngine; using System.Collections.Generic; public class InventoryManager : MonoBehavior { public static InventoryManager Instance { get; set: } private void Awake() { if(Instance!=null && Instance !=this) { Destory(gameObject);return; } Instance = this; } public List<ItemSO> itemList;//所有已经捡起来的物品的集合 public void AddItem(ItemSO item)//当我们捡起物品时将他们放入上面那个列表中 { itemList.Add(item); } }
当玩家拾取一个物品时,可以通过调用 InventoryManager.Instance.AddItem(item) 将物品添加到背包中。
- 控制人物将物品捡起:创建一个脚本挂在人物主角上面,然后也创建一个新的文件夹存放所有的管理(manager)分类(文件管理这件事自己做好就行了):通过碰撞的方式来捡物品:
using UnityEngine; public class PlayerPick : MonoBehavior { if(collision.gameObject.tag==Tag.INTERACTALBE) { PickableObject po=collision.gameObject.GetCompoent<PickableObject>(); if(po!=null)//确定要捡起的物品不是空 { InventoryManager.Instance.AddItem(po.itemSO);//将物品的属性信息放到仓库中 Destroy(po.gameObject); } } }
消耗品上面有刚体有碰撞器所以是可以捡起来的;
-
OnTriggerEnter 是 Unity 提供的一个生命周期方法,当一个带有 Collider 的对象进入另一个带有 Collider 和 Rigidbody 的对象的触发器区域时调用。
这里使用触发器来检测玩家与物品的碰撞。 -
背包系统UI:在创造UI的那个下面创建一个空物体,叫做InventoryUI,在他的下面再创建一个子物体UI,背包是一个列表的形式,所以要先创造一个背景image叫做bg(背包)(在UI子物体下面创建)type要注意是sliced的,然后增加一个新的布局方式,右键UI,选择UI,然后选择scroll view,然后将这个创建的东西放在bg下面,可以将这个当中本身的image取消了,这样透明框就没有了,然后吧水平滚动的也删掉,只剩下上下滚动,在viewport下面的content可以存放我们的滚动列表,然后我们在这里面创建imageUI,在scroll view中的scroll rect组件中,取消勾选horizontal,然后点击content,增加一个组件vertical layout group,在这个组件中勾选最后一行的hight,在增加一个组件在content中,叫做content size fitter将第二个选项选择min size;
-
背包内展示物品UI:在这个itemUI上创建UIimage,将物品的图片放进去,创建text展示名字,创建text展示获取方式,创建text展示数目(这个可以先没有),然后将这个item做成预制体放到预制体文件夹中,创建一个脚本来管理打开背包的UI,控制背包的显示和隐藏:这串代码挂在inventoryUI上,
public class InventoryUI : MonoBehaviour { public static InventoryUI Instance { get; private set; } private GameObject uiGameObject; private GameObject content; public GameObject itemPrefab; private bool isShow = false; private void Awake() { if (Instance != null && Instance != this) { Destroy(gameObject); } Instance = this; } void Start() { uiGameObject = transform.Find("UI").gameObject; content = transform.Find("UI/ListBg/Scroll View/Viewport/Content").gameObject; Hide(); } public void Update() { if (Input.GetKeyDown(KeyCode.B)) { if(isShow) { Hide(); isShow = false; } else { Show(); isShow = true; } } } public void Show() { uiGameObject.SetActive(true); } public void Hide() { uiGameObject.SetActive(false); } public void AddItem(ItemSO itemSO)//捡起物品后背包中添加物品,实例化item { GameObject itemGo = GameObject.Instantiate(itemPrefab);//实例化出一条content item itemGo.transform.parent = content.transform;//并且把实例化的东西放在content里面 ItemUI itemUI = itemGo.GetComponent<ItemUI>(); itemUI.InitItem(itemSO); }
之前写的UI代码是当作单例模式存在的所以这个我们也应该写成单例模式;
-
uiGameObject:背包界面的根对象,用于控制整个UI的显示和隐藏。
-
itemPrefab:物品的预制体,用于实例化背包中的每个物品。
-
使用 GameObject.Instantiate 方法实例化一个物品预制体。
-
itemGo.transform.parent = content.transform; // 并且把实例化的东西放在content里面(将实例化的东西变成content的子对象);
-
transform.Find 的主要作用是在当前对象的子对象中查找具有特定名称的子对象。如果找到匹配的对象,它会返回该子对象的 Transform 组件;如果没有找到,则返回 null。
-
做一个控制item自己跟新的代码放在item上面:
public class ItemUI : MonoBehaviour { public Image iconImage;//对需要跟新的ui属性进行一个引用 public TextMeshProUGUI nameText; public TextMeshProUGUI typeText; private ItemSO itemSO; public void InitItem(ItemSO itemSO)//更新属性括号里面是之前创建的数据载体中的东西 { string type = ""; switch (itemSO.itemType) { case ItemType.Consumable: type = "可消耗品"; break; } iconImage.sprite = itemSO.icon; nameText.text = itemSO.name; typeText.text = type; this.itemSO = itemSO; } public void OnClick() { InventoryUI.Instance.OnItemClick(itemSO); } }
- 补充知识:预制体如何创建:选中你配置好的游戏对象。将其直接拖拽到
Assets
窗口中的某个文件夹内(如Prefabs
文件夹)。Unity会自动将其保存为一个预制体文件 ; -
在编辑器中实例化
将预制体从Assets
窗口拖拽到Hierarchy
窗口,即可在场景中创建一个实例; -
如果你需要在运行时动态创建预制体的实例,可以通过脚本实现,然后将脚本添加到你想要实例化的预制体上。例如:
using UnityEngine; public class SpawnPrefab : MonoBehaviour { public GameObject prefab; // 将预制体拖拽到这个字段 void Start() { // 在指定位置实例化预制体 Instantiate(prefab, new Vector3(0, 0, 0), Quaternion.identity); } }
- sprite是图像;
任务系统:
- 找一个npc用来接取任务叫做taskNPC,然后将其无关的collider删了,标签选择可交互,增加组件胶囊体碰撞体,增加组件nav mesh obstacle,将shape改为capusle,修改它的大小(修改他的其他数据)就可以变成障碍物了;
- 创建一个新的脚本:
public class TaskNPCObject : InteractableObject { public string npcName; public GameTaskSO gameTaskSO; public string[] contentInTaskExecuting; public string[] contentInTaskCompleted; public string[] contentInTaskEnd; protected override void Interact() { switch(gameTaskSO.state) { case GameTaskState.Waiting: DialogueUI.Instance.Show(npcName,gameTaskSO.diague,OnDialogueEnd); break; case GameTaskState.Executing: DialogueUI.Instance.Show(npcName,contentInTaskExecuting); break; case GameTaskState.Completed: DialogueUI.Instance.Show(npcName,contentInTaskCompleted,OnDialogueEnd); break; case GameTaskState.End; DialogueUI.Instance.Show(npcName,contentInTaskEnd); break; default: break; } } public void OnDialogueEnd() { switch(gameTaskSO.state) { case GameTaskState.Waiting: gameTaskSO.Start(); InventoryManager.Instance.AddItem(gameTaskSO.startReward); break; case GameTaskState.Executing: break; case GameTaskState.Completed: gameTaskSO.End(); InventoryManager.Instance.AddItem(gameTaskSO.endReward); break; case GameTaskState.End; break; default: break; } }
放在这个taskNPC上面,将根据下面创建的任务数据放在这个代码里面,然后将不同的对话内容从unity中写入;
-
创建一个脚本用来保存任务相关信息:
public enum GameTaskState { Waiting, Executing, Completed, End } [CreateAssetMenu()] public class GameTaskSO : ScriptableObject { public GameTaskState state;//每个任务的状态 public string[] diague;//任务开始之前和npc的对话 public ItemSO startReward;//保存任务开始时获得的物品和任务完成后获得的物品 public ItemSO endReward; public int woodCount=10; public int CurrentWoodCount=0; public void Start()//检测任务进行到哪里了 { state=GameTaskState.Executing; } public void End() { state = GameTaskState.End; } public void CheckTaskCompletion() { if (CurrentWoodCount >= woodCount) { state = GameTaskState.Completed; // 设置任务状态为完成 End(); // 调用结束任务的方法 } } public void CollectWood() { CurrentWoodCount++; CheckTaskCompletion(); // 每次收集木材后检查是否完成任务 } }
创建好这个代码之后就可以将任务的数据存放了,点击菜单栏创建一个gametaskSO,改名字叫做FirstTask,状态时waiting,然后将对话放上去,然后将刚开始可以获得的物品和结束之后可以获得的物品数据托上去(从DataSO中寻找)
音效:
- 我们需要的音效有:背景音乐,走路音效,获得物品音效,接取任务音效,如果要有什么重要的过场剧情什么的就再弄一个背景音乐;
- 创建一个脚本代码:
using UnityEngine; public class AudioManager : MonoBehaviour { public static AudioManager Instance { get ; privtate set; } public AudioClip walk; public AudioClip pick; public AudioClip task; private void Awake() { Instance = this; } public void PlayWalk(Vector3 position) { AudioSourse.PlayClipAtPoint(walk,position,1f);//这个数字是声音的大小 } public void PlayPick(Vector3 position) { AudioSourse.PlayClipAtPoint(pick,position,1f); } public void Playtask(Vector3 position) { AudioSourse.PlayClipAtPoint(task,position,1f); } }
然后在unity中进行赋值,要在对应的代码中调用并且发出声音时,只需要在相应代码处调用实例即可;
AudioManager.Instance.PlayWalk(transform.psition);
暂停:
- 创建imageUI,增加一个暂停的图标,记得选择对齐方式,在这个创建的UI组件中增加一个button组件,创建一个UIpanel,自己挑一个颜色,在panel下面在创建一个panel缩小成整个屏幕大小的四分之一(这是做一个暂停后的界面)放在左边,然后在这个panel下面创建一个button,但是我们压根不用文字,所以把它携带的文字删掉,创建两个个按钮(继续游戏,退出游戏),第一个制作的panel是要隐藏掉的,在第一个创建的panel上创建一个父物体,父物体叫做pauseUI,将passbutton也放在他的下方,然后物品位置如下图:
- 增加一个暂停界面显示和隐藏的动画:首先点击菜单中的windows 找到animation创建一个animation,然后点击创建,将他放到animation文件夹下面,叫做passUIShow,先打开见面左上角的红点(录制),首先做背景面板的动画(pausepanel),在第一帧上,先启用pausepanel(就是打对勾打两次然后让对勾变成红色即为成功启用),透明度设置为0,然后任意选一帧(25左右)看具体做出来的效果自行判断,这一帧上,(也要先用上述方法启用一次组件),将透明度设置成210的,然后修改我们的子面板(panel),第一帧调解x轴把面板移除游戏界面外,25帧将面板再移回来,然后就可以运行看一下了。然后我们开始做隐藏的动画:点击红点下方的三角,创建一个新的ainimation叫做passUIHide,先点击录制,第一帧就在屏幕内的位置(记得要启用组件panel),然后第25帧把他移到外面去(这里注意要将组件panel禁用掉),然后移出屏幕外的时候,pausepanel面板在第一帧透明度调整为210,第25帧弄成0。
- 做完动画之后点击游戏画面菜单上的animator找到蓝图,创建一个空状态设置为默认状态(不显示UI界面的状态),右键该状态选择第二个选项,将其设置为了默认状态(就是橙色的),然后具体连线如下图:
- 连好蓝图之后我们要弄条件,在左边栏左上角找到parameters并点击,点击加号增加一个bool值叫做IsShow,然后点击蓝图中new到show的线,然后将它的状态设置成如下图所示:
- 那么从show状态转化成hide状态的话就变成false(跟上述步骤相同,只不过将图中的true改为了false),hide转化为show状态跟上述图片保持一致;
- 然后我们开始写控制这些状态的代码,并将其放到pauseUI下面:
using UnityEngine; public class PauseUI : MonoBehaviour { private Animator anim; void Start() { anim=GetComponent<Animator>(); } public void OnPauseButtonClick() { Time.timeScale=0;//控制停止正在运动的物体 anim.SetBool("IsShow",true); } public void OnContinueButtonClick() { Time.timeScale=1; anim.SetBool("IsShow",false); } public void OnExitButtonClick() { Application.Quit(); } }
然后将这些方法放在对应的按钮上:用pausebutton举例:如下图所示:
- 找到文件中的animation找到hide和show,将其loop time全部关掉,然后点开passUI找到animator,找到update mode,选择动画跟新的模式,选择最后一个选项(表示不会受到到时间缩放的影响);
- 但是此时我们打开暂停界面的话,人物还是会移动的,所以我们要修改我们的行走代码,然后就可以控制只要打开UI界面就不能走路的判断代码,但是我不会修改,所以这件事交给了kimi:
结束:
- (提示,每增加一个UI记得对齐对齐啊啊) 在canvas下面创造一个panel,叫做GameOverUI,然后下面创造一个子物体叫做UIParent,双击canvas点击2d模式编辑一下结束UI,颜色透明度自己设置,然后新增加一个文本框,直接写个gameover。
- 增加一个按钮,用来退出游戏,然后命名情况如下图所示:(这里将假设UIParent是不存在的因为这个是之后他新加的然后忽略UIParent的情况下这个名称分别是这样的分布)
- 然后我们要加入进入结束UI的动画,点击GameOverUI,然后场景中点击animation开始做动画(跟之前录制那些都是一样的),用和之前一样的方法,首先点击录制,在第一帧上启用UIParent(启用的那里会变红)然后找到图像组件将透明度变成0,并且将gameover字体和按钮都拖出屏幕外以达到一个类似于ppt动画的效果,第三十帧左右,将透明度设置成251,并且将刚刚移出去的文字和按钮移回来,这样就做好了;
- 点击animator控制动画何时播放的状态,然后默认将UIParent禁用,然后动画方面跟上面暂停一模一样往下做就行(详细见暂停笔记的34567);
- 然后代码是这样的:
using UnityEngine; public class GameEndUI : MonoBehaviour { private Animator anim; void Start() { anim=GetComponent<Animator>(); } public void OnExitButtonClick() { Application.Quit(); } }
判定游戏如何结束:
- 游戏结束的话我想要将地图设计成一个长廊形式,一步步走,然后走到最后,当制作一定数目的物理道具之后,便可以达成结局(跟最后一个npc对话)然后进入结局,那么我们怎么样就知道有多少道具了呢,可以用背包检测带有标签是建造物的物品有多少,然后就可以成功了;
开始:
- 控制按钮代码:
using UnityEngine; using UnityEngine.SceneManagement; public class MenuControl : MonoBehaviour { public void StartGame() { SceneManager.LoadSceneAsync(1); } public void Exit() { Application.Quit(); } }
然后这个我弄把,弄两个按钮就好了;
捡起物品:
- 代码:
using UnityEngine; public class ItemPickup : MonoBehaviour { public Item item; private void OnTriggerEnter(Collider other) { if (other.CompareTag("Player")) { // 拾取物品并添加到背包 InventoryManager.instance.AddItem(item); Destroy(gameObject); } } }
将其挂载到要拾取的物品上面;
-
新的背包管理器:
using UnityEngine; using System.Collections.Generic; public class InventoryManager : MonoBehaviour { public static InventoryManager instance; public List<Slot> slots = new List<Slot>(); public GameObject slotPrefab; public Transform slotsParent; private void Awake() { if (instance == null) { instance = this; } else { Destroy(gameObject); } } public void AddItem(Item item) { bool itemAdded = false; // 遍历背包中的槽位,查找相同物品或空槽位 foreach (Slot slot in slots) { if (slot.item != null && slot.item.itemName == item.itemName) { // 如果槽位中有相同物品且未堆满,则增加数量 if (slot.item.currentQuantity < item.maxStack) { slot.item.currentQuantity++; slot.UpdateDisplay(); itemAdded = true; break; } } else if (slot.item == null) { // 如果槽位为空,则添加物品 slot.item = item; slot.UpdateDisplay(); itemAdded = true; break; } } // 如果背包已满,创建新的槽位并添加物品 if (!itemAdded) { GameObject newSlotGO = Instantiate(slotPrefab, slotsParent); Slot newSlot = newSlotGO.GetComponent<Slot>(); newSlot.item = item; newSlot.UpdateDisplay(); slots.Add(newSlot); } } }
物品分类:
using UnityEngine; [System.Serializable] public class Item { public string itemName; public Sprite icon; public string description; public int maxStack = 1; public int currentQuantity = 1; }
改进后可以跟新的背包UI:
using UnityEngine; using System.Collections.Generic; public class InventoryManager : MonoBehaviour { public static InventoryManager instance; public List<Slot> slots = new List<Slot>(); public GameObject slotPrefab; public Transform slotsParent; private void Awake() { if (instance == null) { instance = this; } else { Destroy(gameObject); } } public void AddItem(Item item) { bool itemAdded = false; // 遍历背包中的槽位,查找相同物品或空槽位 foreach (Slot slot in slots) { if (slot.item != null && slot.item.itemName == item.itemName) { // 如果槽位中有相同物品且未堆满,则增加数量 if (slot.item.currentQuantity < item.maxStack) { slot.item.currentQuantity++; slot.UpdateDisplay(); itemAdded = true; break; } } else if (slot.item == null) { // 如果槽位为空,则添加物品 slot.item = item; slot.UpdateDisplay(); itemAdded = true; break; } } // 如果背包已满,创建新的槽位并添加物品 if (!itemAdded) { GameObject newSlotGO = Instantiate(slotPrefab, slotsParent); Slot newSlot = newSlotGO.GetComponent<Slot>(); newSlot.item = item; newSlot.UpdateDisplay(); slots.Add(newSlot); } // 更新背包UI UpdateInventoryUI(); } public void RemoveItem(Item item) { // 遍历背包中的槽位,查找并移除物品 for (int i = slots.Count - 1; i >= 0; i--) { if (slots[i].item == item) { if (item.maxStack > 1) { item.currentQuantity--; if (item.currentQuantity <= 0) { slots[i].item = null; } } else { slots[i].item = null; } slots[i].UpdateDisplay(); break; } } // 更新背包UI UpdateInventoryUI(); } private void UpdateInventoryUI() { // 清空所有槽位 foreach (Slot slot in slots) { slot.item = null; slot.UpdateDisplay(); } // 重新生成槽位 for (int i = 0; i < slots.Count; i++) { if (slots[i].item != null) { slots[i].UpdateDisplay(); } } } }
拼图游戏:
- 要想进入拼图游戏,思路是这样的:首先在要制造的物品附近放一个制作的交互按钮,然后制作点击按钮然后就可以切换场景的效果,然后切换之后在另一个场景之中制作拼图游戏;
- 在制造的物品附近放的按钮的代码是这个,记住拖动按钮的onclick方法时,是将脚本拖拽的对应的物品拖拽到那个框里,不是把脚本拖进那个框里:
using UnityEngine; using UnityEngine.SceneManagement; public class GameControl : MonoBehaviour { public void StartGame() { SceneManager.LoadSceneAsync(2); } }
然后如果后面有很多拼图游戏的话,切换场景的时候只要把中间这个数字一直往后面改就行了,这个脚本呢挂在main camara上吧,或者创造一个空物体,自己取个名字,怎么样都可以的;
- 切换新场景之后可以增加一个背景图片:这个我觉得可以弄成游戏里面的实际场景然后将他虚化一点点,到时候截图就可以了;
- 将新场景换成2d,然后将要拼图的图片放入ps中(右键用ps打开)用controlshift+j切割切割成16片将每个图片分别导出成png(在图层那里右键直接导出就行),然后把纹理类型全部改成sprite(UI和2d),然后关闭天空盒,天空盒如右图所示:
- 然后将一整张的图片拖进场景中,将主图的透明度降低,然后将其余的碎片放入主图上,跟主图对应起来,然后创建两个空对象,一个叫做隐藏,一个叫做显示,把碎片门都拖到这个隐藏中,然后再将这些碎片复制一遍拖入显示中,然后把隐藏取消激活了,然后就将显示下面的所有碎片拖出来随便摆放;
- 然后开始添加UI,添加一个旧版按钮,叫做退出,用来退出该场景切换到上一个场景中;(文字都在创建好按钮的下方文本中输入即可);
- 然后再添加一个UI文本,写“完成!!!”表示拼图结束拼好了;
- 新建一个空对象,叫做exit把刚刚创建的所有UI放到这个的下面(包括canves);
- 隐藏所有的UI,将puzzleMG脚本拖进显示里面,然后在脚本中将隐藏好的ExitUI拖入脚本之中;
- 然后打开隐藏和显示下方分别有四个碎片,四个碎片要增加碰撞器,分别都添加polygon collider 2d组件,并且检查绿色框框有没有对应上;
- 然后再显示下面的碎片中每一个都增加puzzlepiece脚本,并且将相对应的隐藏中的碎片拖入脚本中position中;
- 然后在Exit上面拖上代码endevent;
- 最后将所有的点击事件都放在按钮上面;
- puzzel piece代码下面可以调整拼图最小的判定范围,自己改改就行了;
- 所需要的代码有:
using System.Collections; using System.Collections.Generic; using UnityEngine; public class EndEvent : MonoBehaviour { public void EndGame() { SceneManager.LoadSceneAsync(1); } }
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.SceneManagement; public class PuzzleMG : MonoBehaviour { public static PuzzleMG instance; public List<PuzzlePiece> puzzles = new List<PuzzlePiece>(); public GameObject gameOverPanel; void Start() { instance = this; puzzles.Clear(); puzzles.AddRange(GetComponentsInChildren<PuzzlePiece>()); } public void Check() { foreach (PuzzlePiece puzzle in puzzles) { if (!puzzle.isComplete) { return; } } // 过关逻辑 Debug.Log("恭喜过关!"); gameOverPanel.SetActive(true); } }
using UnityEngine; public class PuzzlePiece : MonoBehaviour { public bool isComplete; // 是否已完成拼图 private bool isDragging = false; // 是否正在拖拽 private Vector3 offset; // 鼠标点击位置与拼图块中心的偏移 private Camera cam; // 主摄像机 public Transform targetPosition; // 目标位置 public float snapThreshold = 0.5f; // 吸附阈值 void Start() { cam = Camera.main; // 获取主摄像机 } void OnMouseDown() { if (isComplete) return; // 如果已完成拼图,则不处理 isDragging = true; // 开始拖拽 offset = transform.position - GetMouseWorldPosition(); // 计算偏移 } void OnMouseUp() { if (isComplete) return; // 如果已完成拼图,则不处理 isDragging = false; // 结束拖拽 CheckPosition(); // 检查位置 } void Update() { if (isDragging) { transform.position = GetMouseWorldPosition() + offset; // 更新拼图块位置 } } // 获取鼠标在世界空间中的位置 private Vector3 GetMouseWorldPosition() { Vector3 mousePoint = Input.mousePosition; // 获取鼠标位置 mousePoint.z = cam.WorldToScreenPoint(transform.position).z; // 获取Z坐标 return cam.ScreenToWorldPoint(mousePoint); // 转换为世界坐标 } // 检查拼图块是否接近目标位置 private void CheckPosition() { if (Vector3.Distance(transform.position, targetPosition.position) < snapThreshold) { transform.position = targetPosition.position; // 如果接近,则吸附到目标位置 isComplete = true; // 设置已完成拼图 PuzzleMG.instance.Check(); } } }
但是第一个代码只要有了这个按钮就可以随时随地结束游戏,不过也行,我们本来就需要添加一个判断游戏成功的标志,然后点击退出游戏,就会得到相应的奖励;