00.视角调整
Top-Down 自上而下视角游戏,要将 Edit > Project Settings > Graphics 的 Transparency Sort Mode 设置为 Custom Axis,设置 Transparency Sort Axis 为(0,1,0)。
主摄像机的 Projection 属性设置为 orthographic 正交视角。
在摄像机上挂载 Pixel Perfect Camera 组件,可以让2D像素风游戏画面更加整洁清晰。
03.导入素材
导入的 Sprite 素材修改设置如下,可以创建储存 Preset 方便所有素材统一设置。
04-05.角色移动
添加 Sorting Group 和 Box Collider 2D 和 Rigidbody 2D 组件。
角色和其他刚体碰撞后可能旋转,可以勾选 Rigidbody 2D > Constraints > Freeze Rotation 避免。
06-08.地图编辑/摄像机跟随
可创建 Sprite Atlas 打包所有地图瓦片。
2023.1之后的版本新增 Brush Picks 功能,可以更方便地绘制瓦片地图。
09.碰撞层和景观树
碰撞层:
Collisions 图片文件的碰撞边缘调整:
预制体 Collision 取消 Tilemap Renderer 勾选,以保证运行后隐藏碰撞层。
为 Collision 添加 Tilemap Collider 2D组件,每个瓦片都会生成单独的碰撞体;为了优化碰撞,再添加Composite Collider 2D,勾选 Tilemap Collider 2D 选项 Used by composite,可以把瓦片的独立碰撞体合并为一个整体。
添加 Composite Collider 2D 后,会自动添加 Rigidbody 2D 组件,默认设置会导致运行时碰撞层掉落,需要将 Rigidbody 2D 中 Body Type 改为 Static。
资料:Body Type 三种类型 Dynamic / Kinematic / Static 的区别
景观树:
树干:修改 SpriteSortPoint 为 pivot,以保证和角色的正确层级关系;修改 pivot 位置。
树冠:添加 Animator 和 Animation,复制创建多个树时,修改 Animator 里的 Entry 指向。
10.摄像机边界
为 CM vcam1 添加 Cinemachine Confiner,添加脚本 Switch Bounds,每次加载不同场景时,自动读取当前场景中 Tag 为 BoundsConfiner 的物体作为摄像机边界。
预制体 Bounds 添加 Polygon Collider 2D 组件,修改 Tag,编辑碰撞边缘,勾选 Is Trigger;勾选后仅检测与场景中的其他刚体是否存在碰撞关系,但不产生实际的碰撞反应;如果不勾选的话,运行后角色会被挤出 Bounds 区域。
资料:Collider(碰撞器)与IsTrigger(触碰器)详解
11.景观物体遮挡半透明
为树干和树整体 Prefab 添加 Box Collider 2D 组件,勾选 Is Trigger;树干不勾选 Is Trigger。
为树干和树冠添加 Item Fader 脚本,为角色添加 Trigger Item Fader 脚本。
资料:DOTween 基础教程 | 常用函数
12.背包数据初始化
资料:MVC模式详解
创建脚本 DataCollection,添加类 ItemDetails 存储物品详细属性;
资料:[System.Serializable] 的作用 | [SerializeField] 的作用
创建脚本 Enums 用于定义道具类型;
创建脚本 ItemDataList_SO 使用 ScriptableObject 存储道具的各项属性值。
13-17.物品编辑器相关
新建 UI Toolkit > Editor Window,在 UI Builder 中设计编辑器样式。
ItemEditor 脚本中修改其在菜单中的位置:[MenuItem("XXXX/ItemEditor")]
18-19.库存管理
新建单例 Singleton 脚本,新建 InventoryManager 脚本继承单例,以保证在游戏运行时有且仅有一个库存管理实例,持续存在的场景中创建 InventoryManager 预制体,挂载对应脚本,调用时直接使用类名即可。
资料:单例模式详解
新建 Item 脚本,编写 Init 方法,确保地图中出现新物品时,自动调整物品的碰撞体尺寸和位置偏移(因为物品通常要把锚点 pivot 设置为 bottom )。
和物品有关的脚本都要添加 MFarm.Inventory 命名空间。
20-22.物品拾取/背包结构/背包添加物品
新建 Item Pick Up 脚本实现物品拾取逻辑,挂给 Player 预制体。
在 DataCollection 脚本中新建结构体 InventoryItem,存储背包中的物品信息(ID和数量)。
资料:class 和 struct 区别和用法 | Part 2 | Part 3
23-24.制作UI:Action Bar(底部物品栏)/背包
创建 Button 类型的 Slot_Bag 预制体,包含子元素:Image、Amount 数量、Highlight 选中高亮;
使用此预制体拼UI界面 Action Bar 和 Player Bag,Player Bag 额外显示金币数量和头像。
25.SlotUI根据数据显示图片和数量
为 Slot_Bag 新增脚本 SlotUI;
自定义两个方法 UndateEmptySlot 更新空格子显示、UpdateSlot 更新格子显示;
在 Start 方法中默认格子初始不选中,此外如果此格子没物品,更新空格子显示。
资料:Unity常用标记字段 如:Header / Range | Part 2
26.背包UI显示
创建脚本 InventoryUI,挂在 Canvas 下的 Inventory 上;
在此脚本中挂载 Inventory 下的 Action Bar 和 Player Bag 的所有 Slots,在 Start 方法中为每个格子编号。
创建脚本 EventHandler,用于管理游戏的所有事件。
资料:生命周期函数 如:Start / OnEnable / Update
27.控制背包打开关闭
相关方法写在 InventoryUI 脚本里,脚本添加 Player Bag 预制体。
为背包按钮增加 Button 组件,为 OnClick 添加调用方法。
28.背包物品选中高亮
SlotUI 继承类 IPointerClickHandler,会自动生成对应方法 OnPointerClick。
在 InventoryUI 脚本中新增方法 UpdateSlotHighlight,实现点击物品切换选中/未选中状态。
资料:Event Systems 中的 IPointer_?_Handler
29.创建 DragItem 实现物品拖拽跟随显示
SlotUI 继承类 IBeginDragHandler,IDragHandler,IEndDragHandler。
创建与 Inventory 同级的 DragCanvas,勾选 Override Sorting,修改 Sort Order 为 2;
为 DragCanvas 创建子物体 DragitemImage(默认关闭),取消勾选 Raycast Target,防止图片屏蔽鼠射线;
取消勾选 Slot_Bag 所有子物体的 Raycast Target,方便后边实现物品交换。
30.背包内物品交换/把物品扔到地图上
SlotUI 中的 OnEndDrag 方法中判断拖拽物品结束的位置是否是其他 Slot,如果双方类型均为背包,则调用 InventoryManager 中的 SwapItem 方法,交换双方位置。
创建脚本 ItemManager 并挂在持续存在的场景中,在 InstantiateItemInScene 方法中实现在世界地图上生成物品,如果拖拽物品结束的位置是世界地图(非 Slot )、且物品属性为可扔下时,调用该方法。
31-32.制作 ItemToolTip(物品提示)UI,关联信息并显示
拼UI时使用组件 Vertical Layout Group 和 Content Size Fitter;
金币数额使用 Legacy Text。
为 ItemToolTip 预制体添加脚本,SetupTooltip 方法获取 UI 需要显示的信息;
为 Slot_Bag 添加 ShowItemToolTip 脚本,继承类 IPointerEnterHandler,IPointerExitHandler;在代码中修改 ItemToolTip 锚点以确保其显示在Slot 右上方位置。
使用 LayoutRebuilder.ForceRebuildLayoutImmediate 强制立即更新UI的布局。
为需要动态调整尺寸的文本框添加 Layout Element,设置最小高度。
33-34.制作 Player 动画/实现选中物品触发举起动画
资料:Animator Override Controller 的使用 | 官方文档 | 官方API说明
资料:switch 语法糖 | 什么是语法糖?
资料:C# 字典
35.地图编辑:新增房屋和可砍伐树木
?遗留问题:两个半透明sprite重叠部分颜色加深
36-38.构建时间系统/UI制作/关联数据显示
持续存在的场景中添加 TimeManager;
TimeUI 脚本中注册事件,实现显示时间相关数据在 UI 中展示的方法,包括日期数字 OnGameDateEvent、时间数字 OnGameMinuteEvent、点亮时间格 SwitchHourImage、昼夜交替图旋转 DayNightImageRotate;
在 TimeManager 中触发事件,更新时间 UI 显示。
Sprite 设置中的 Read/Write 勾选可以确保点击图片的透明区域无反应。
39-41.绘制第二场景/场景切换/跨场景移动及信息处理
EventHandler 中定义事件 TransitionEvent 场景切换、AfterSceneLoadedEvent 场景加载后、BeforeSceneUnloadEvent 场景卸载前、MoveToPositionEvent 角色位置移动。
持续存在的场景中添加 TransitionManager,注册并实现场景切换事件,LoadSceneSetActive 方法实现启动游戏时加载设定场景,注意在此方法中需要触发 AfterSceneLoadedEvent 以确保首个场景能正常加载摄像机边缘。
在多个脚本中按照需求注册并实现场景卸载前/加载后的事件,分别实现下列功能:
- InventoryUI:场景卸载前,取消物品栏选中高亮,Before
- AnimatorOverride:取消手持+切换默认待机动画,Before
- ItemManager:找到 itemParent,After
- Player:切换场景的过程中角色不可控,Before/After;改变角色位置,Move
场景的出入口设置碰撞、勾选Trigger,在 OnTriggerEnter2D 中触发场景切换事件。
资料:协程 IEnumerator 介绍 | Part 2 | StartCoroutine 和 Yield
资料:SceneManager 场景管理器 API | Part 2
42.制作 [SceneName] 特性
为 Inspector 中需要选择场景的地方制作显示所有场景名称的下拉菜单。
43.场景切换淡入淡出
新建Canvas > Fade Panel,添加 Canvas Group 组件,通过改变其 Alpha 值实现效果。
在 TransitionManager 中创建 Fade 协程方法实现效果,并在 Transition 方法中调用。
注意:2023.1 版本后 FindObjectOfType / FindObjectsOfType 方法已被弃用,可以使用 FindObjectByType 替换。
44.保存和加载场景中的物品
卸载场景前,读取场景中的物品并储存,重新加载场景时,按照储存内容重新生成。
使用字典存储场景中的所有物品,key 是场景名字,value 是场景中所有物品组成的列表。
由于Unity中的Vector3无法直接被序列化,因此需要在 DataCollection 脚本中新建 SerializableVector3 类,将物品坐标序列化,以便用于存储。
在 DataCollection 中创建 SceneItem 类,定义物品 id(类型为 int )和坐标(类型为自定义的 Vector3 类)。
在 ItemManager 中创建字典,GetAllSceneItems 方法用于获取并存储当前场景的所有物品,判断字典中是否已经存储当前场景的物品列表,如果有则更新列表,如果没有则新增;RecreateAllItems 方法用于重新生成当前场景的所有物品,判断字典中是否存在和当前场景匹配的 key,如果存在则根据 value 重新创建物品,在此之前需要先销毁当前场景的所有物品避免重复。
45.鼠标指针根据物品调整
鼠标图片如果设置 texture type 为cursor,会无法实现调整大小颜色等功能,因此依然选择 Sprite,自行创建 Image 来跟随鼠标的位置实现 cursor 的效果。
创建与 Fade Panel 同级的 Cursor Image,隶属于 Cursor Canvas( Tag 设置为同名),必须取消勾选其 Raycast Target 属性。
修改图片素材,让鼠标尖固定在同个位置,之后更改 Cursor Image 锚点。
创建 CanvasManager 脚本,添加各种鼠标指针资源,添加事件 OnItemSelectedEvent 根据选中物品类型切换对应鼠标图片,判断鼠标指向是否为其他UI,在 Update 方法中实时更新鼠标位置和图片;
先对图片设置一个跟随,然后根据当前是什么物品然后切换不同的指针图片,并且在选中物品和取消选中时改变。然后还需要注意如果当前鼠标指向的地方是UI界面,则让其恢复为默认状态。
46-48.构建地图信息系统/生成地图数据/设置鼠标可用状态
实现功能:指针选中道具栏中的物品并移动到地图上,判断指针指向的瓦片属性,显示对应的指针样式,并触发对应方法(比如挖坑/种植等)。
为地图瓦片添加不同功能的方法:
方法1:创建规则瓦片 Rule Tile 并为关联挂载了对应脚本的物体;每个瓦片都要挂载脚本,仅适用于地图较小的情况。
方法2:在场景中的 Grid 预制体中添加 GridInformation 脚本;仅局限于将一个格子设置成单一的属性,但实际游戏中每个地块可能有多个属性,比如可以种地和放置物品。
实际解决方案:编写挂载在 Grid Properties 的脚本,代表不同的地图块逻辑,然后像绘制地图 Collision 那样,单独绘制一层,并将其信息存储到 ScriptObject 中,再关联实际地图。
DataCollection 脚本中创建类 TileProperty,成员包括瓦片坐标、瓦片类型、bool值 TypeValue 类型:代表特性当前是否生效(玩家对地块进行某些操作后可能改变此值)。
创建 MapData_SO 脚本,成员包括场景名、TileProperty 列表;为每张地图创建不同的 MapData_SO 文件。
像绘制地图 Collision 那样,绘制四层对应不同瓦片特性的地图,代表在此层绘制区域对应的地图中,可以实现对应的操作。
创建 GridMap 脚本,挂在四种瓦片特性的预制体上,实现功能:在编辑模式中开启 GridProperty 预制体(画地图)时,清空SO文件数据;关闭 GirdProperty 预制体(地图画完)时,遍历当前层的所有瓦片,保存其属性并添加到地图数据SO文件中。
DataCollection 脚本中创建类 TileDetails,成员包括瓦片网格坐标,代表允许执行四种操作的bool值,与种植相关的多种天数统计。
创建 GridMapManager 脚本,定义字典,字典的key为场景名,value为 TileDtails 列表;遍历SO文件内容,获得地图的所有功能瓦片信息并保存在字典中。
修改 TransitionManager 的 Start 方法为 IEnumerator 类型,不再用 StartCoroutine 加载场景(暂时没搞明白目的),在此方法内触发场景加载后事件。
在 CursorManager 脚本中新增bool值 cursorEnable,订阅场景卸载前和加载后的事件,实现以下功能:场景卸载前,设置鼠标不可用,场景加载完成后,鼠标才可用。
修改 CursorManager 脚本,实现功能:选中道具栏中的物品后,鼠标移动到地图上时显示不同指针样式并判断执行后续操作。
在 CheckCursorValid 方法中,需要先把鼠标屏幕坐标转换成地图的网格坐标,根据网格坐标获取瓦片信息,再获取鼠标选中的物品信息,再获取角色位置信息,先判断角色位置与鼠标目标位置的距离是否小于物品的可使用范围,再根据瓦片信息和物品信息设置鼠标指针样式。
资料:他人笔记
49-50.实现鼠标选中物品后的场景点击事件流程/实现扔下物品功能
实现功能:在选中物品的情况下,地图中点击鼠标左键,触发后续流程,例如:选中斧头点击树,角色播放砍树动画,树播放被砍动画,地图新增掉落物等等。
在 EventHandler 中新增三个事件,分别为:①鼠标点击事件,②角色动画播放完事件,③扔下物品事件。
在CursorManager 脚本的 Update 中检测玩家是否点击鼠标左键,之后触发通知①鼠标点击事件。
在 Player 脚本中订阅①鼠标点击事件,事件处理方法为:播放角色动画,之后触发通知②角色动画播放完事件。
在 GridMapManager 脚本中订阅②角色动画播放完事件,事件处理方法为:判断物品类型和瓦片类型,再触发通知③扔下物品事件。
在 ItemMananger 脚本中订阅③扔下物品事件,事件处理方法为:在地图对应瓦片位置生成物品。
在 InventoryMananger 脚本中订阅③扔下物品事件,事件处理方法为:移除指定数量的物品(新增方法RemoveItem)。
修改 SlotUI 的 UpdateEmptySlot 方法,以确保物品从背包扔完后清除高亮状态并清空物品信息,触发通知物品选中事件。
模拟扔出效果的方法:让物品以弧线、阴影以直线移动到目标位置。
以 ItemBase 为基准,修改为 BounceItemBase,添加子预制体 ItemShadow,添加脚本根据物体生成阴影。
为BounceItemBase 增加 ItemBounce 脚本,移动过程中关闭碰撞,InitBounce 方法根据目标点坐标和方向计算移动距离;Bounce 方法实现移动过程,在Update中调用。
在ItemManager脚本中,获得角色位置,在扔物品事件的实现方法中,计算并传递参数给 InitBounce 方法。
51.实现挖坑和浇水的地图更改变化
为 GripMap 中的 Dig 和 Water 层添加对应Tag,创建新的规则瓦片 Dig Tile 和 Water Tile,并在 GridMapManager 中获得这两个规则瓦片。
修改 CursorManager 脚本,CheckCursorValid 方法中根据选中工具和地块状态设置鼠标指针显示。
修改 GridMapManager 脚本,在角色动画播放后事件中根据选中工具和地块状态设置显示对应位置的挖地/浇水瓦片。
52.使用工具动画
在 Player 下创建覆盖动画 Tool,继承 BaseController,按照下列步骤修改 BaseController:
新增参数:useTool(Trigger类型)、mouseX 和 mouseY(float类型);
新增 BlendTree UseTool,连线为:AnyState (触发 useTool) > UseTool > Exit(代表回到 Entry 循环);
修改 Animator Override 脚本,在 OnItemSelectedEvent 方法中根据所选物品判断动作类型,并将所有身体部位的动画切换至此动作类型的控制器。
资料:Animation 与 Animator | 动画过渡参数
53.地图随时间变化刷新显示
修改 GridMapManager 脚本,在对地图瓦片执行操作后,使用 UpdateTileDetails 方法保存修改后的瓦片信息;在进入场景时,使用 DisplayMap 方法将之前保存的瓦片信息重新显示在地图上。
新增事件 GameDayEvent 用于处理和每天相关的信息(例如刷新地图/作物生长),在 TimeManager 脚本中天数+1并计算完季节后调用事件(相当于每天1次);
在 GridMapManager 脚本中注册事件,实际处理方法 OnGameDayEvent 为:修改字典中存储的瓦片信息(比如挖坑/浇水已过天数),用 RefreshMap 方法先清空对应层的所有瓦片,再按照新的瓦片信息重新显示地图。
54-56.种植:种子数据库/实现播种/种子成长过程
创建脚本 CropDetails 定义所有和作物生长相关的参数,创建基于此类的 SO 文件。
修改 CursorManager 脚本,在 CheckCursorValid 方法中判断选择的物品为种子,显示对应的鼠标样式。
新增 PlantSeedEvent 事件,在 CropManager 脚本中注册事件,实际处理方法为:判断当前选中物品是否为种子、当前是否是可种植季节、当前地图块是否未种下种子,全部满足时,更新瓦片信息,在此瓦片上显示作物。
创建新预制体 CropBase,挂载对应脚本 Crop,把Crop SO文件中的不同阶段物品Prefabs 修改为 CropBase。
修改 GridMapManager 脚本,在 OnExecuteActionAfterAnimation 方法中判断选择的物品为种子,之后调用对应事件,同时更新背包物品数量;在 DisplayMap 方法中增加种子的显示;在 RefreshMap 方法中,删除所有挂载了 Crop 脚本的预制体。
为 DropItemEvent 的事件增加新参数 itemType,当其值为种子时,不在地图中生成新物品。
57-59.菜篮子收割/收割获得果实/作物重复收割
使用道具栏物品的通用逻辑:
在 CursorManager 脚本的 CheckCursorValid 方法中判断选中物品后指向的地图瓦片,显示相应的鼠标指针状态;
如果鼠标点击会触发角色行为变化,则修改 Player 脚本的 MouseClickedEvent 实现方法,让 Player 做出对应的反应,例如切换播放动画,否则直接跳转到下一步;
修改各个脚本中的 ExecuteActionAfterAnimEvent 实现方法,让相应物体做出反应,比如地图瓦片被挖坑、被浇水、出现种子;
调整其他关联事件,比如背包中物品数量的增减,地图状态更新,农作物切换生长状态等等。
在 GridMapManager 脚本中增加 GetCropObject 方法获取鼠标位置的农作物对象,如果不为空,处理工具行为;
在 CropManager 脚本中为当前的农作物赋值属性;
在 CropDetails 脚本中增加方法 CheckToolAvailable 检查工具是否可用,增加方法 GetTotalRequireCount 获得工具需要的使用次数,例如收割用菜篮子1次,挖矿用镐5次等,以便后期扩充其他采集项;
在 GridMapManager 脚本中增加 GetCropObject 方法获取鼠标位置的农作物对象;
在 Crop 脚本中新增 ProcessToolAction 方法处理工具行为,判断针对当前作物的采集计数是否已经达到了此作物设定的采集所需计数,如果满足条件,则在对应位置生成采集物品。
在 InventoryManager 脚本中注册生成采集物品事件,以完成在背包中添加物品的功能;
在 AnimatorOverride 脚本中注册生成采集物品事件,以完成在头顶显示农作物1秒的功能。
可再生农作物的处理:在 Crop 脚本中的 SpawnHarvestedItem 方法里判断当前瓦片的作物是否是可再生的,如果是且作物没有达到最大生长次数,则重置作物的生长天数,再刷新地图显示;如果不是,则清空当前瓦片存储的作物信息,并删除地图上的作物。
60-63.制作砍树动画/实现砍树功能/随机生成掉落物
把砍树相关的元素添加到物品数据库中,包括种子、木材、树根(用于在作物数据库里添加转换新物品ID)。
由于树成熟之后需要砍伐,成熟的树应该为此前做好的树根与树冠分离的预制体,为 Tree01 预制体挂载 Crop 脚本,修改作物数据库对应种子的最后1个生长阶段的预制体和显示阶段。
为树干部分增加动画控制器,添加四个 Trigger,用于砍树向左/向右摇晃、向左/向右倒下。
在 Crop 脚本中的 ProcessToolAction 增加判断玩家位置与树的关系,判断触发对应动画。
修改 CropManager 脚本的 DisplayCropPlant 方法,在生成新的 cropPrefab 时传入对应的瓦片信息,这是因为树生长的最后阶段其碰撞体范围会发生变化,但是地图瓦片信息不变,如果不保存原始的瓦片信息,则在树成熟后砍树,点击树干部分时,会获得鼠标指向的新的瓦片信息,但是此瓦片并非树生长所在的瓦片,数据可能为空,导致后续流程出现问题。
修改 GridMapManager 脚本的判断物品使用功能,新增砍树判断。
以 CropBase 为基础创建 CropBase Variant 作为树干的预制体,并为其子物体添加新的碰撞体。
创建新脚本 ActionBarButton,挂到 UI 层 ActionBar 的所有格子上,修改其对应按键。
64-68.粒子效果对象池/树叶掉落、敲石头、割草粒子特效制作/实现割草功能
砍树的粒子效果需要频繁生成和销毁,因此使用对象池方法提高性能。
增加枚举 ParticleEffectType,用粒子的文件名定义枚举值;
在持续存在场景中增加PoolManager,添加对应脚本;
修改Crop脚本的 ProcessToolAction 方法,在砍树触发摇晃效果后调用播放粒子效果事件。
如果在场景中放棵树,但在加载场景后会发现消失了,这是因为场景加载时会执行 RefreshMap 方法,删除场景中的所有农作物并重新按照存储的瓦片信息生成。解决方法是在开始执行刷新地图时调用新事件,这个事件会把地图中已有的树的种子ID设定好。
创建 CropGenerator 脚本,挂载给所有需要在加载场景后保留的物品,比如石头和树,注册 GenerateCropEvent 事件,以实现修改并保存此物品所在的瓦片信息并更新瓦片字典。
注意由于逻辑是保存物品的瓦片信息再重新生成物品,因此新生成的物品和最初摆放的位置会有轻微偏移。
完成上述操作后,加载场景时会再次执行事件导致重复产生树,解决方法是用字典存储每个场景是否是首次加载,如果是才执行上边的初始化场景的方法。
修改 GridMapManager 脚本中的 OnAfterSceneLoadedEvent 方法,判断是否是首次加载当前场景,如果是就调用 GenerateCropEvent 事件,再刷新地图。
割草待补充
资料:对象池
79-80.与NPC对话
创建 DialogueCanvas 并挂载 DialogueUI 脚本,内容包括左侧头像+名字、右侧头像+名字、对话文本、下句提示,在 Inspector 面板中拖拽绑定。
创建 DialoguePiece 脚本,定义对话相关的变量。
创建 DialogueController 脚本,挂载给所有会触发对话的NPC。
拓展:可以自定义SO文件,然后从中读取对话,不用在每个NPC身上单独配置。
资料:Stack 中的 TryPeek 和 TryPop 区别
81-83.对话后打开商店/实现交易流程
实现功能:对话后打开商店,并且商店UI根据NPC挂载的物品数据显示
创建商店UI Bag Base,它和 Player Bag 的区别在于玩家背包中的格子数是固定的,即使没有物品,格子也会全部显示,但商店的格子只会根据是否有出售物品显示,因此要为其创建单独的预制体 Slot_Shop,修改 Slot Type 为 Shop,删除 Slot Holder 下的所有内容,并通过代码读取商店中的物品列表,再在UI中生成对应实例。
创建脚本 NPC Function 挂载在NPC身上,专门用来处理对话结束事件 OnFinishEvent 后调用的方法,比如打开商店。
在 InventoryUI 脚本中注册事件 OnBaseBagOpenEvent,根据格子类型获得对应的prefab,遍历对应的SO文件,在商店UI的第一个子对象中(也就是Slot Holder)实例化prefab,并获得其 SlotUI 组件,之后将其加入商品列表中并更新UI显示。
实现功能:背包打开时禁止角色移动
新增枚举 GameState,内容为两种游戏状态:Pause 和 GamePlay。
在Player脚本中注册事件,实现当游戏状态为Pause时,角色不可移动;
在 DialogueController 脚本中的 DialogueRoutine 方法中,以及 NPCFunction 脚本中的 OpenShop 方法中,调用游戏状态更新事件。
实现功能:打开商店时同时打开玩家背包,两者并排显示
修改 InventoryUI 脚本,修改 OnBaseBagOpenEvent 方法,修改背包和商店的UI位置,教程里用的是改中心点,我用的是改锚点位置。
实现功能:购买/出售二次确认,输入数量
修改 SlotUI 脚本,在 OnEndDrag 方法中新增条件判断,以触发购买物品事件和出售物品事件。
制作 TradeUI 并挂载对应脚本,在 InventoryManager 中新增 TradeItem 方法,在 TradeUI 中点击确认按钮时,触发此方法。
在 InventoryUI 脚本中同步更新金币数显示。
84-86.家具图纸数据/实现放家具流程/保存家具布局
功能拆分:购买图纸>点击图纸,消耗材料,生成物品,举在头上>点击地图位置,生成家具
实现功能:背包有图纸时,鼠标悬浮显示所需材料
物品编辑器中新增图纸,增加新的SO脚本并生成对应文件 BluePrintData_SO,添加以下数据:图纸ID+所需材料+世界场景prefab。
在 ItemToolTip 的UI里附加材料面板 ResourcePanel,在 ItemToolTip 脚本中新增方法
SetupResouresPanel 用于设置材料面板的显示内容,在 ShowItemToolTip 的鼠标悬停功能中添加判断悬停对象是否为家具,如果是就传递数据给材料面板。
实现功能:检测鼠标点击蓝图后,家具图片会跟随鼠标移动,点击地图生成家具,更新背包
修改 CursorManager 脚本,在 OnItemSelectedEvent 方法中判断当前选中物品是否是家具蓝图,如果是就显示家具图片,在 CheckCursorValid 方法中添加建造图片跟随鼠标指针移动的功能,以及在switch语句中增加物品类型为Furniture的判断,鼠标指向的瓦片可以放置家具(需要提前绘制 canPlaceItem 瓦片层)、所需材料充足,则在地图中生成家具。
添加 HaveFurnitureInRadius 方法,确保要放置的家具碰撞范围内没有其他家具,此外要把所有场景中的 Bounds 的 Layers 设置为 IgnoreRaycast 无视鼠标射线,否则检测会出问题。
注意:务必在SetCursorValid 和 InValid 方法中添加判断启动buildImage,不然点击物品栏后会触发Update中的关闭buildImage,之后再也不会启动,视频中因为用的是快捷键选择家具,没有发现此问题。
InventoryBag_SO 脚本中添加 GetInventoryItem 方法,实现通过ID查找库存物品的功能。
InventoryManager 脚本中添加 CheckStock 方法,实现根据输入ID检测其对应的材料是否充足。
修改 GridMapManager 脚本,switch case 添加家具判断,在地图上生成家具(ItemManager),移除背包图纸和对应材料(InventoryManager),两个脚本中分别注册并实现 OnBuildFurnitureEvent 事件。
实现功能:切换场景保存和读取场景中的家具
新增脚本 Furniture 挂载给所有家具的prefab;
使用类似保存和加载场景中农作物的方式,在 ItemManager 中新增两个方法,分别为 GetAllSceneFurnitures 和 RecreateAllFurnitures,分别在卸载场景前和加载场景后事件调用。
86-87.储物箱存储功能/箱子与背包数据交换/箱子数据保存
和创建木椅的流程相同,添加储物箱。
创建SO文件 BoxBagTemplate 作为每个储物箱的基础模板;
为所有储物箱的prefab添加 Box 脚本,添加碰撞范围内触发显示右键提示,点击右键打开箱子的功能。
在 InventoryManager 脚本中添加 SwapItem 的重载方法,用于交换背包和箱子的物品;
在SlotUI 脚本中的 OnEndDrag 判断拖拽起始格子类型和结束格子类型,并调用 SwapItem 方法。
实现功能:切换场景时保存箱子的数据
在 InventoryManager 中创建字典,用来保存所有场景中的箱子;新增 GetBoxData 方法用于通过key从字典里获得某个箱子的数据列表,新增 AddBoxDataDict 方法用于把箱子加入字典中。
在 ItemManager 脚本的 GetAllSceneFurnitures 方法中判断家具列表中是否有箱子,如果有就为其boxIndex赋值。
在 Box 脚本中新增 InitBox 方法,判断是要在字典中新建条目,还是读取已有条目。
修改 ItemManager 脚本的 OnBuildFurnitureEvent 方法,让家具生成时就修改其 boxIndex,并调用 InitBox 方法;修改 RecreateAllFurnitures 方法,在创建家具的时候,使用已经存储的 boxIndex 初始化箱子。
88-89.URP与环境光:昼夜交替实现
创建 URP Asset,在 Project Settings 中设置默认渲染管线,在 Window>Rendering>Converter 中转换所有材质的渲染管线为 2D URP;
在场景中创建 Light>Global 2D,修改粒子的prefab的 Renderer>Material为Sprite-Lit-Default。
创建 LightPatternList_SO脚本,生成对应的SO文件。
创建 LightControl 脚本,挂载给每个场景中的每个需要有光照切换的预制体,添加方法 ChangeLightShift,根据传入的季节/时间段/时间差切换光照的颜色等等;时间差用来计算光照过渡的持续时间;
修改TimeManager的 UpdateGameTime 方法,在小时变动时,通知调用切换光照事件;
创建 LightManager 脚本,注册并实现 ①切换光照事件:保存传入的季节/时间段/时间差,遍历场景内的光照并切换;②场景加载后事件:遍历当前场景中的光照并切换。
资料:TimeSpan 详解
资料:DOTween 使用方法 | Part 2
90-92.添加播放音效/渐入效果/对象池播放音效
添加 AudioManager 和子物体 GameMusic 背景音乐 / AmbientMusic 环境音,为子物体添加 Audio Source 组件。
创建 SoundDetailsList_SO 文件,用于保存全部声音文件。
创建 SceneSoundList_SO 文件,用于配置各个场景所需的BGM和环境音。
创建 AudioManager 脚本,注册场景加载后事件,在切换场景后播放音乐和环境音。
创建 AudioMixer,Snapshot 添加三个状态,分别代表常规、只有环境音、静音;暴露Groups下的 Master/Amibient/Music 音量变量,修改 Audio Source 组件的 output,将其与 Audio Mixer 内的元素绑定,再通过暴露的变量实现切换场景的音量渐变。
资料:Audio Mixer 详解 | 复杂应用
创建 Sound 预制体,添加 AudioSource 和对应脚本,添加方法 SetSound,获得传入的 soundDetails 对应的音效和音量。
修改 PoolManager 脚本,注册初始化音效事件,实现把 soundDetails 传给 Sound 预制体,并从对象池中取出的功能。
在 CropDetails 类中新增 SoundName 变量,实现根据采集的农作物播放相关音效的功能。
在 Crop 脚本中的 ProcessToolAction 中调用初始化音效事件。
问题:如果使用自带的 ObjectPool 方法,会导致初次播放时没有声音,此外游戏中可能出现多种音效同时播放的情况。
解决方案:在 PoolManager 中自定义方法,实现创建对象池、获取对象池中的对象、释放对象。
创建脚本 AnimationEvent 挂载给 Player > Body,在 Animation 的指定帧添加播放走路音效。
新增 PlaySoundEvent 并在 AudioManager 脚本中注册并实现根据功能:根据传入的 SoundName 播放对应音效。
在每个需要播放音效的地方调用 CallPlaySoundEvent,例如树倒下、挖坑、种种子、捡东西等等。
资料:对象池 ObjectPool 详解 | Part 2
93-95.Timeline制作开场动画/对话轨道显示内容/控制暂停和启动
在持续存在的场景中创建 IntroCanvas,添加所需图片资源,创建 Timeline 文件,将 IntroCanvas和背景音乐拖入,录制 Animation;之后将 Panel 拖入并创建 Active Track。
创建一系列脚本,用于在Timeline中添加自定义的对话轨道。
- DialogueBehaviour 脚本:继承 PlayableBehaviour 类,定义自定义轨道中创建的 Clip 包含的数据内容,重写 Timeline 的所有生命周期方法。
- DialogueClip 脚本:继承 PlayableAsset 和 ITimelineClipAsset 类,以实现在自定义轨道中添加 Clip 的功能。
- DialogueTrack 脚本:继承 TrackAsset 类,并规定可在此轨道添加的 Clip 类型,以实现在 Timeline 窗口中加入自定义轨道功能。
创建 TimelineManager 脚本,拖拽传入 Timeline 文件,注册加载场景后事件,实现功能自动播放 Timeline;添加 PauseTimeline 方法用于暂停 Timeline,以便在 DialogueBehaviour 脚本中调用。
资料:Timeline 详解
96.创建主菜单UI
97-99.存档管理:保存/加载
创建 GameSaveData 类定义所有需要存档的数据。
创建接口 ISaveable,类型是 interface,用于定义方法和属性,但不提供实现,继承此接口的类必须实现接口中的所有方法。
创建 DataGUID 类,挂载此脚本的元素Awake时,生成全局唯一标识符。
所有需要存档的内容,包括Player、Inventory / Item / Time / GridMap Manager,都要挂载DataGUID脚本,并继承接口 ISaveable 实现其方法,同时在 Start 方法中注册到 SaveLoadManager 的列表中。
在 SaveLoadManager 脚本中新增 Save 和 Load 方法,使用 Newtonsoft Json 中的方法实现保存游戏数据到本地json文件和加载功能。
资料:Json数据操作方法 | 三种 Json 处理方案对比
资料:数据本地持久化方案 PlayerPrefs / Json / XML
资料:interface 作用
100-101.暂停菜单和返回逻辑
实现功能:存档信息显示+点击存档条目加载
DataSlot 脚本中定义 DataTime 和 DataScene,根据传入的GUID获得存档的时间和场景信息;
SaveLoadManager 脚本中增加 ReadSaveData 方法,用于从本地获得json文件数据并回传给存档栏位;
SaveSlotUI 脚本中添加方法 SetupSlotUI 根据存档位置获得存档数据,并显示时间和场景在UI中;注册点击监听事件方法 LoadGameData,实现点击存档栏位加载对应游戏进度的功能。
实现功能:开始新游戏
添加新事件 StartNewGameEvent,在点击存档空栏位时调用该事件。
在以下脚本中注册事件:
- Player:禁止操作、设置初始位置
- InventoryManager:初始化玩家背包和金币、清空箱子数据
- ItemManager:清空场景物品和家具
- TimeManager:初始化时间
- TransitionManager:加载初始场景
- NPCManager:重置NPC位置/场景
- LightManager:初始化时间段
实现功能:暂停和返回逻辑、修改音量
创建UIManager脚本装载给 MainCanvas,在游戏开始时加载主菜单,在加载场景后销毁主菜单。
新建暂停面板,添加音量控制 Slider、返回标题 Button、退出游戏 Button,在两个按钮的 onClick 添加对应功能。
为暂停按钮注册监听点击事件,打开暂停面板。
为音量控制添加值改变时间,调用 AudioManger 中的设置音量方法。
实现功能:返回标题
添加新事件 EndGameEvent,在点击暂停菜单中的返回标题时调用该事件。
在以下脚本中注册事件:
- Player:禁止玩家操作
- TimeManager:时间暂停
- AudioManager:静音
- SaveLoadManager:保存当前进度
- TransitionManager:卸载当前场景
其他资料
他人笔记:简易图文版 01-45 | 种植系统 46 | A*寻路算法
其他: