双语机翻视频:
https://www.bilibili.com/video/av34383045/
在官网asset Store下载了完整工程,和pdf
asset Store:https://assetstore.unity.com/account/assets
pdf:https://unity3d.com/cn/learn/tutorials/projects/adventure-game-tutorial
在AssetStore上下载资源并导入项目:(my Asset入口)
https://jingyan.baidu.com/article/4f34706e758caee387b56d3e.html
导入后说Animation Clip __preview__Take 001有负time keys,无法压缩,可能看起来异常
Lighting data asset ‘LightingData’ 与当前版本不兼容。Please use Generate Lighting to rebuild the lighting data.(需要rebuild lighting data)Realtime Global Illumination cannot be used until the lighting data is rebuilt.(否则实时全局光照Realtime Global Illumination不能使用)
目录
Player Control(点击行走)
Build a click to move animated character using:
• EventSystem
• NavMesh
• Animator
• Prefabs
Inventory(库存)
Build a UI and Item Management System for Player Inventory
• UI System
• Editor Scripting
Interaction System(交互系统)
Build a system allowing the Player to interact with the game
• Conditions
Create a system to check the current game state
• Scripting Patterns
• Scriptable Objects
• Generic Classes(泛型类)
• Inheritance(继承)
• Extension Methods(扩展方法)
• Reactions
Create a system to perform actions based on condition state
• Polymorphism(多态)
• Further editor scripting
• Serialization(序列化)
• Interactables
Create a system to define what the player can interact with
• Interactable Geometry
• EventSystem
• Interaction System Summary
Game State(游戏状态、改变场景)
Creating a system to load scenes while preserving game state
• Scene Manager
• ScriptableObjects as temporary runtime data storage(脚本化对象作为临时的运行时数据存储)
• Delegates(委托)
• Lambda Expressions(表达式)
Event System:点击、对话
Navmesh(导航网格)
Animator(行走动画)
Goal
INTERACTABLE互动,通过检测条件触发不同事件
Scene Management:Load Scene ,Maintain Inventory & Scene State(加载场景并保存库存和场景状态)
Understanding the architecture(体系结构) of Scene Loading
Persistent(持久) Sceneà Load Level Additive-Scene 1à Unload Scene- Scene 1
à Load Level Additive-Scene 2à Set Active Scene-Scene 2
持久场景load 、unload其他场景
When the user clicks on the ground, the character must move to that location(点击地面,角色移动到地点)
Interactable objects in the scene will be provided to our team for our character to interact with.
When an Interactable is clicked on, the character should approach the interactionLocation of the Interactable (点击交互对象,到达交互地点)
interactionLocation is a Transform value saved as part of the Interactable
On arrival, the character should match the position and rotation of the interactionLocation and call the Interact function of the Interactable(角色匹配坐标与旋转,调用交互函数)
When required, the character must play various animations in response to trigger parameters sent by the Interactables; specifically the supplied trigger parameters: HighTake, MedTake, LowTake and AttemptTake
The character cannot be allowed to move while these animations are playing(动画播放时,角色不能移动)
P94
NavMesh :to define the walkable areas in the levels(导航网格:定义可行走区域)
Event Systems: to detect and handle user input and scripted interaction(事件系统:检测处理用户输入,脚本交互)
Animator state machine :to control and play all of the character animations; including idle(闲置), walking and interaction(动画状态机:控制、播放所有角色动画)
Prefab system :to save the character so it can be easily added and used in any scene in the game(可以保存角色,并放到任意场景)
打开项目
1. With the correct Asset Store package imported:
1/6 - Adventure Tutorial - The Player
2. Navigate to the Project window(project窗口)
3. Expand the Scenes folder(Scenes文件夹)
4. Open the SecurityRoom scene(双击打开场景)
将场景中room对象设为静态:
1. Navigate to the Hierarchy window(层级窗口)
2. Expand(展开) the SecurityRoom GameObject and select
the SecurityRoomEnvironment GameObject
3. Check the Static checkbox(在Inspector窗口名字旁边选择Static复选框设为静态场景:在上面创建导航网格nav mesh,不移动这些几何体)
4. Select Yes, change children(将该对象与其子对象全部设为静态)
将一些影响光照的子对象取消静态(不能走到这些东西上):
1. Expand the SecurityRoomEnvironment
2. Multi-Select the children called:
• BlackUnlit
• FloorLightGlow
• HologramLight
• HologramLight02
• SecurityGateBeams
3. Uncheck the Static checkbox
设置nav mesh
1. Open the Navigation window(window-navigation打开navigation窗口,可以在场景视图中看到蓝色网格)
2. Select the Bake panel
3. Set the Max Slope to 1
4. Set the Step Height to 0.2
5. Under ‘Advanced’ set Height Mesh to true(nav mesh、high mesh可以估测每个部分的高度,更精确地走过颠簸)
6. Press the Bake button(在右下角保存)
1. Navigate to the Inspector window(切换视图)
2. Save the scene(保存场景)
We need to be able to interact with our environment
• An event system has 3 requirements.(事件系统需要:发送、接收、管理) Something to -
1. Send events
Physics Raycaster component(触发事件:物理射线投射组件。附加在camera上,每一帧重新投射进场景,寻找基础物理对象)
2. Receive events
Colliders & Event Triggers(碰撞器和事件触发器,碰撞器hip with Raycaster后,向事件触发器发出信号)
3. Manage events
The EventSystem
创建EventSystem管理事件(Manage events):
1. Navigate to the Hierarchy
2. Create > UI > Event System(添加Event System对象)
3. Add an AudioListener component(在该对象上添加组件,作为event system的监听者。在实际项目中放在持久性场景,监听所有场景,而不在每个场景中建立 )
4. Save the scene
创建Send events:
1. Expand the SecurityRoom and CameraRig GameObjects
2. Select the Camera GameObject
3. Add a Physics Raycaster component.(在相机上添加Physics Raycaster组件,查找组件:Phy或在event目录下)- Make sure this is not a Physics 2D Raycaster
创建Receive events:
1. Select the SecurityRoom GameObject
2. Add a MeshCollider component(在SecurityRoom增加网格碰撞器)
3. Set the Mesh property to SecurityRoomMeshCollider(设置组件下的网格属性,为该资源)
1. Add an EventTrigger component(在SecurityRoom增加事件触发器)
2. Click Add New Event Type and select (创建Pointer Click类型的Event)
3. Click the + button to add an event(添加新事件留着引用脚本)
4. Leave the event empty!
5. Save the scene
处理玩家本身和它的动画状态
添加Animator Controller:
1. Navigate to the Project window
2. With the Animators > Player folder selected, create an Animator Controller
(在工程窗口的Animators >Player文件夹创建动画控制器(Animator Controller))
3. Name it ClickToMove
创建参数
1. Open the Animator window(双击Animator Controller文件)
2. Select the Parameters panel
3. To create a new Parameter, select the + dropdown menu
4. Create a float parameter and name it Speed
1. Create 4 trigger parameters and name them:
• AttemptTake
• HighTake
• MedTake
• LowTake
Animator Controller决定什么时候,播放哪些动画,移动速度
参数拼写很重要,将匹配其他脚本和material
1. Navigate to the Layout area(指网格区域)
2. Right-click on the background (在空白处右击)and select:
Create State > From New Blend Tree(融合树)
3. Select the new Blend Tree
4. In the Inspector window, name the state Walking
5. Double click on the state to edit the blend tree
1. Click on BlendTree
2. Rename BlendTree to WalkingBlendTree
3. To make a new Motion Field, click the + button and
select Add Motion Field
4. Repeat this 2 more times to make a total of
3 Motion Fields in all
1. Using Circle Select find & assign:(圆形按钮,添加引用)
• PlayerIdleBlender
• PlayerWalk
• PlayerRun
(当速度低于行走速度阈值时,将行走于闲置融合)
1. Uncheck Automate Thresholds(取消下方自动处理阈值复选框)2. Click on the Compute Thresholds dropdown menu
3. Select Speed(不是设置,而是一次计算,改变值后可再一次选择它计算,选择Automate Thresholds相当于重置)4. Return to Base Layer using Breadcrumb
Route motion 路线运动:每个animation也可以让角色移动
速度speed可以控制动画播放的速度
添加动画:
1. Navigate to the Project window
2. Expand the Animations > Player folder
1. Select Idle, TakeObjectAttempt & TakeObjects(同时选择这三个文件)
2. Drag these into Animator window(拖进网格窗口)
3. Arrange Animations as shown:
添加状态转变Transition:
1. Right click on Walking
2. Select Make Transition(右击新建状态转变)
3. Left click on PlayerIdle
This makes a Transition from Walking to PlayerIdle
4. Make a Transition from PlayerIdle back to Walking
1. Make Transitions from Any State to each of the
PlayerTakeObject... states:
• Attempt
• High
• Mid
• Low
2. Make Transitions from each of the PlayerTakeObject...states to Walking
设置Transition触发条件:
1. Select the Transition from Walking to PlayerIdle
2. Uncheck has exit time(动画完成退出时间)
3. Under the condition list click the + button to add a
condition and set the parameters as:(添加速度范围作为条件)
• Speed (already set)
• Less
• 0.1
1. Select the Transition from PlayerIdle to Walking
2. Uncheck has exit time
3. Under the condition list click the + button to add a
condition
• Speed (already set)
• Greater (already set)
• 0.1
1. Select each of the Transitions from Any State to each of the PlayerTakeObject... states
2. Add a new condition and set the parameter the appropriate trigger using the parameter dropdown:(发生事件时触发,默认播放完毕后自动转为walking)
• AttemptTake
• HighTake
• MedTake
• LowTake
1. Select PlayerIdle
2. Set the tag to Locomotion(移动)
3. Select Walking
4. Set the tag to Locomotion
5. Navigate to the Scene window
6. Save the scene
拿出player模型:
1. Navigate to the Project window
2. Expand the Models folder
3. Drag the Player model asset into the Hierarchy
4. Set the Layer on the Player GameObject to Character(Layer属性在Tag右侧)
5. Set Position -0.7, 0.0, 3.5
6. Set the Rotation 0.0, 180, 0.0
设置Animator组件属性:
1. Using Circle select - set Controller to ClickToMove(在Animator组件的Controller属性框)
2. Add a new component: NavMeshAgent
3. Change the Speed property to 2
4. Change the Acceleration(加速度) property to 20
5. Change the Stopping Distance to 0.15(到目的地前的减速距离)
添加脚本,放入预制件:
1. Select and Drag the Player GameObject to the Project window > Prefabs folder
2. Navigate to Scripts/Monobehaviours/Player folder
3. Create a new C# Script called PlayerMovement
4. Drag the PlayerMovement script onto the Player Prefab
5. Open the PlayerMovement script for editing
https://www.bilibili.com/video/av34383045/?p=2
pdf:P139
Scripts/Monobehaviours/Player folder
创建PlayerMovement脚本,添加到Player(预制件,而非场景中的示例)
脚本控制Player瞄准目的地移动, 由导航网格(nac mesh驱动)
同时控制动画,在接近目的地时放慢
编写脚本:
需要包含一个新的namespace:using UnityEngine.EventSystems;
(在Unity5.5之后,需要加上: using UnityEngine.AI;
需要引用一个animator、nav mesh agent(导航网格代理)
需要知道要到多远去
不允许agent旋转角色:agent.updateRotation = false;
在互动时禁止移动:co-routine
确定角色要等待多久
确定玩家目的地:存储玩家目的地位置的Vector3
靠近目的地时停止,在那时的第一帧将速度设为很小
新建控制角色如何移动的函数:OnAnimatorMove
结合animator与网格代理的移动:基于动画播放速度,设置导航网格代理的速度-route motion:
agent.velocity = animator.deltaPosition / Time.deltaTime;
.deltaPosition:角色这一帧移动距离
.deltaTime:两帧之间时间差
V=s/t
还需要slowing、stopping
确定是否要停止:调用private void Stopping
需要控制的参数:(out float speed)
类似地,函数Slowing需要传入out float speed,
需要判断目的地距离,传入float distanceToDestination
旋转角色:private void Moving ()
在Update函数中:
agent.pathPending代表网格代理正在计算path。这时直接返回
否则已经有path
需要知道沿path走多快
Agent有两个速度:实际、将要
float speed = agent.desiredVelocity.magnitude;
magnitude(幅度)
依次检查是否需要停止、减速、移动
确定停止距离比例常量:private const float stopDistanceProportion = 0.1f;
if (agent.remainingDistance <= agent.stoppingDistance * stopDistanceProportion)
Stopping (out speed);
确保转身之前速度够快:speed > turnSpeedThreshold
(转身速度阈值)
需要设置Animator速度(需要字符串匹配,可以用整数代替)
声明只读整数(只读整数为哈希值):private readonly int hashSpeedPara = Animator.StringToHash("Speed");
Speed要匹配animator中的参数
设定变速时间(Damp time of speed)默认值:public float speedDampTime = 0.1f;
animator.SetFloat(hashSpeedPara, speed, speedDampTime, Time.deltaTime);
Stopping函数中:
停止网格代理:agent.Stop();
固定精确位置:transform.position = destinationPosition;
设置速度:speed = 0f;
Slowing函数中:
停止网格代理,
自己定义对位置的控制,渐渐移动:Vector3.MoveTowards(当前位,目标位,速度)
transform.position = Vector3.MoveTowards(transform.position, destinationPosition, slowingSpeed * Time.deltaTime);
定义slowingSpeed
比例(1-距/停止距):float proportionalDistance = 1f - distanceToDestination / agent.stoppingDistance;
线性插值:speed = Mathf.Lerp(slowingSpeed, 0f, proportionalDistance);
Moving函数:
完成旋转
得到目标旋转值:Quaternion targetRotation = Quaternion.LookRotation(agent.desiredVelocity);
在当前、目标旋转值间以速度旋转transform.rotation = Quaternion.Lerp(transform.rotation, targetRotation, turnSmoothing * Time.deltaTime);
OnGroundClick函数:
处理点击地面-public void OnGroundClick(BaseEventData data)
BaseEventData有此次点击时发生事件的数据
转换类型:PointerEventData pData = (PointerEventData)data;
在导航网格上找到最接近点击处的点:
需要距离private const float navMeshSampleDistance = 4f:
NavMesh.SamplePosition (pData.pointerCurrentRaycast.worldPosition, out hit, navMeshSampleDistance, NavMesh.AllAreas)
参数:欲到达的世界坐标系中一个点,返回所有信息的接收变量,sample结束的距离,导航网格中可以用的区域
设定目标位置为hit位置:destinationPosition = hit.position;
若无hit位置则为最接近的位:destinationPosition = pData.pointerCurrentRaycast.worldPosition;
让导航网格代理使用该位置:agent.SetDestination(destinationPosition);
恢复代理(之前用.Stop停止过):agent.Resume ();
Interact交互变量还未使用
1. Select the Player Prefab
2. On the PlayerMovement component:
• Setup the reference to the Player's Animator
• Setup the reference to the Player's NavMeshAgent
在预制件中的Player的PlayerMovement组件中填充animator、agent引用预制件中的Player
1. Navigate to the Hierarchy
2. Select SecurityRoom GameObject
3. Find the Event Trigger
4. Drag the Player GameObject from the Hierarchy onto
the object field of the Event Trigger
5. Select PlayerMovement.OnGroundClick()
Event trigger引用:
在层级目录中,SecurityRoom的组件Event trigger,使用player的实例,调用PlayerMovement中的OnGroundClick
room的碰撞器将触发Event Trigger,找到玩家和该方法
Test
在前往时,确定什么是可互动的(interactable)
创建变量存储他们:private Interactable currentInteractable;
当到达目的地时,使用他们互动
Stopping函数中:
判断可互动
if (currentInteractable)
{
面朝交互方向:transform.rotation = currentInteractable.interactionLocation.rotation;
currentInteractable.Interact();
确保互动只被调用一次:currentInteractable = null;
StartCoroutine (WaitForInteraction ());
}
Bool handleInput只有在互动时才处理输入
判断是否可以移动,动画机是否在运动状态(locomotion)
-使用哈希方法:
private readonly int hashLocomotionTag = Animator.StringToHash("Locomotion");
协程处理要交互发生需等待时间:
private IEnumerator WaitForInteraction ()
{
等待时不获取任何输入:handleInput = false;
单位为秒:yield return inputHoldWait;
当不在运动状态就等待,(0为基础层),与缓存的运动标签不同,则等待一帧:while (animator.GetCurrentAnimatorStateInfo (0).tagHash != hashLocomotionTag)
{
yield return null;
}
handleInput = true;
}
Stop中调用协程:StartCoroutine (WaitForInteraction ());
Slowing中平滑改变选择角度(而不在Stop中突然改变):
如果是可交互的,旋转,否则维持当前旋转角度:Quaternion targetRotation =
currentInteractable ? currentInteractable.interactionLocation.rotation : transform.rotation;
transform.rotation =
Quaternion.Lerp(transform.rotation, targetRotation, proportionalDistance);
点击可交互物体时:
public void OnInteractableClick(Interactable interactable)
{
保证可处理输入:if(!handleInput)
return;
存储当前物品的可交互性:currentInteractable = interactable;
设置目的地:destinationPosition = currentInteractable.interactionLocation.position;
使用导航代理:agent.SetDestination(destinationPosition);
agent.Resume ();
}
在OnGroundClick中:
无法处理输入时,点击地面同样无效果:if(!handleInput) return;
当点击可互动物后,又点击地面,改变主意:currentInteractable = null;
1. Interactables, along with Conditions and Reactions,(可交互物,有条件、反应)
have been supplied to our team
2. We simply need to set them up to work with our new Player click to move system
3. Note: We will be taking on the role of developing these systems later in the session
设置Event Trigger:
1. Select PictureInteractable(在层级目录中)
2. Find the Event Trigger component
3. Drag the Player GameObject from the Hierarchy onto the
object field of the Event Trigger
4. Select PlayerMovement.OnInteractableClick()
触发的事件与地面不同
5. Drag the Interactable GameObject or the Interactable
component below onto the EventTrigger's Parameter field
(OnInteractableClick需要传入参数,将下方的脚本拽入参数字段)
using System.Collections;
using UnityEngine;
using UnityEngine.AI;
using UnityEngine.EventSystems;
public class PlayerMovement : MonoBehaviour
{
public Animator animator;
public NavMeshAgent agent;
public float turnSmoothing = 15f;
public float speedDampTime = 0.1f;
public float slowingSpeed = 0.175f;
public float turnSpeedThreshold = 0.5f;
public float inputHoldDelay = 0.5f;
private Interactable currentInteractable;
private Vector3 destinationPosition;
private bool handleInput = true;
private WaitForSeconds inputHoldWait;
private readonly int hashSpeedPara = Animator.StringToHash("Speed");
private readonly int hashLocomotionTag = Animator.StringToHash("Locomotion");
public const string startingPositionKey = "starting position";
private const float stopDistanceProportion = 0.1f;
private const float navMeshSampleDistance = 4f;
private void Start()
{
agent.updateRotation = false;
inputHoldWait = new WaitForSeconds (inputHoldDelay);
destinationPosition = transform.position;
}
private void OnAnimatorMove()
{
agent.velocity = animator.deltaPosition / Time.deltaTime;
}
private void Update()
{
if (agent.pathPending)
return;
float speed = agent.desiredVelocity.magnitude;
if (agent.remainingDistance <= agent.stoppingDistance * stopDistanceProportion)
Stopping (out speed);
else if (agent.remainingDistance <= agent.stoppingDistance)
Slowing(out speed, agent.remainingDistance);
else if (speed > turnSpeedThreshold)
Moving ();
animator.SetFloat(hashSpeedPara, speed, speedDampTime, Time.deltaTime);
}
private void Stopping (out float speed)
{
agent.Stop();
transform.position = destinationPosition;
speed = 0f;
if (currentInteractable)
{
transform.rotation = currentInteractable.interactionLocation.rotation;
currentInteractable.Interact();
currentInteractable = null;
StartCoroutine (WaitForInteraction ());
}
}
private void Slowing (out float speed, float distanceToDestination)
{
agent.Stop();
float proportionalDistance = 1f - distanceToDestination / agent.stoppingDistance;
Quaternion targetRotation = currentInteractable ? currentInteractable.interactionLocation.rotation : transform.rotation;
transform.rotation = Quaternion.Lerp(transform.rotation, targetRotation, proportionalDistance);
transform.position = Vector3.MoveTowards(transform.position, destinationPosition, slowingSpeed * Time.deltaTime);
speed = Mathf.Lerp(slowingSpeed, 0f, proportionalDistance);
}
private void Moving ()
{
Quaternion targetRotation = Quaternion.LookRotation(agent.desiredVelocity);
transform.rotation = Quaternion.Lerp(transform.rotation, targetRotation, turnSmoothing * Time.deltaTime);
}
public void OnGroundClick(BaseEventData data)
{
if(!handleInput)
return;
currentInteractable = null;
PointerEventData pData = (PointerEventData)data;
NavMeshHit hit;
if (NavMesh.SamplePosition (pData.pointerCurrentRaycast.worldPosition, out hit, navMeshSampleDistance, NavMesh.AllAreas))
destinationPosition = hit.position;
else
destinationPosition = pData.pointerCurrentRaycast.worldPosition;
agent.SetDestination(destinationPosition);
agent.Resume ();
}
public void OnInteractableClick(Interactable interactable)
{
if(!handleInput)
return;
currentInteractable = interactable;
destinationPosition = currentInteractable.interactionLocation.position;
agent.SetDestination(destinationPosition);
agent.Resume ();
}
private IEnumerator WaitForInteraction ()
{
handleInput = false;
yield return inputHoldWait;
while (animator.GetCurrentAnimatorStateInfo (0).tagHash != hashLocomotionTag)
{
yield return null;
}
handleInput = true;
}
}
P156
https://www.bilibili.com/video/av34383045/?p=3
Create a simple inventory system with persistent content that is not lost during scene changes
Inventory Items should be extensible if the design changes可扩展的
two public functions: AddItem & RemoveItem两个公共方法
Simplify and improve the workflow of the project in the Inspector with regards to the Inventory and its Items简化、提升工作流
Use the UI system to display the inventory to the user用UI展示库存
Use ScriptableObjects to make a simple Item class which defines every possible inventory item and can easily be extended and referenced by the Inventory构建Item类,定义可能的库存项,使能被Inventory引用
Create a custom inspector for the Inventory to improve the workflow of the project
1. Navigate to the Scenes folder
2. Open the Persistent scene
3. Set the Scene view to 2D mode(Scene视图上侧的2D按钮)
4. Navigate to the Game view
5. Set the Aspect Ratio(长宽比) to 16:9
6. Select and frame the PersistentCanvas
1. The order of UI Elements in the Hierarchy window informs the UI system what order to render the UI Elements(渲染UI元素)
2. The rendering order is from the top to the bottom which will render on screen from the back to the front(渲染次序从上到下,从后到前)
1. Navigate back to the Scene view场景视图
2. With the PersistentCanvas selected:Create an empty child GameObject
3. Name this new GameObject Inventory(在PersistentCanvas创建空游戏对象Inventory)
4. Make sure that it is the first child of PersistentCanvas and that it is above FadeImage(确认排列为第一个子对象,在FadeImage之上,FadeImage是覆盖整个屏幕的淡入淡出黑框)
1. Create a child of Inventory called ItemSlot(Inventory下创建子对象ItemSlot项目槽)
2. As a child of ItemSlot, Create a new UI > Image GameObject(在ItemSlot下创建UI > Image对象)
3. Duplicate the Image GameObject(再复制一个Image对象。)
4. Name the first child Image BackgroundImage
5. Name the second ItemImage(上面的image作为背景图像,下面的Image绘制item图像)
1. Select the BackgroundImage GameObject
2. Set the Image component’s Source Image to InventorySlotBG(在BackgroundImage的Image组件设置图像源为InventorySlotBG)
3. Select the ItemImage GameObject
4. Disable the Image component(禁用ItemImage的Image组件,因为无Source Image的image组件会默认为纯色(白),禁用后能看到背景)
1. Drag the ItemSlot GameObject into the Prefabs folder to make it a prefab(ItemSlot拖入预制件,易修改)
2. Return to the Hierarchy window
3. Duplicate the ItemSlot GameObject 3 times so there are 4 ItemSlots in total(复制出四个ItemSlot槽)
4. Name them: ItemSlot0, ItemSlot1, ItemSlot2 and ItemSlot3(分别重命名)
1. Select the Inventory GameObject
2. Add a Vertical Layout(竖直布局) Group component(在Inventory添加组件Vertical Layout)
1. Find the Inventory’s Rect Transform component(在Inventory的Rect Transform组件调整)
2. Set the width to 135 and the height to 600(设置后item在inventory一列展开)
3. From the anchor selection dropdown, choose middle-right(左上角的框,点开选择,使锚点固定在画布右侧。)
4. Set the position to -95, 0, 0(锚点在整体画布的位置受PosX,PosY控制)
11:32
接下来给Inventory写脚本
文件路径:Scripts/ScriptableObjects/Inventory
[CreateAssetMenu] |
继承ScriptableObject可通过脚本访问对象,则可以将其创建为assetà则有item assets在项目文件夹中,得到创建asset的属性菜单。
则当点击在Unity菜单中点击assets时,可以创建一个item(基于这个脚本)
脚本中唯一定义的变量Sprite sprite,使用spike代替库存中的东西
如果要用它代表Object就加入public GameObject itemObject;来扩展这个item System
控制Inventory
1. Select the Scripts > MonoBehaviours > Inventory folder
2. Create a C# script called Inventory
3. Open the Inventory script for editing
(在上述文件夹创建脚本Inventory)
编辑脚本:
Namespace:
using UnityEngine.UI; //处理UI,允许访问image组件
变量(variables):
需要使用它存储、显示items,需要得到图像组件来显示,需要items的数组存储物品。
相同size,数量(amount)的items,用 image组件显示他们
public const int numItemSlots = 4; // The number of items that can be carried. This is a constant so that the number of Images and Items are always the same. 槽数为UI中物品数 |
使用UI初始化一些恒定长度数组,设为public:希望inventory editor能访问槽数
public Image[] itemImages = new Image[numItemSlots]; // The Image components that display the Items.将引用图像组件,初始化为相同大小数组 |
函数(function):
添加add items进入库存
删除remove
AddItem需要在其他地方被调用àpublic
需要传入添加的itemà参数
遍历所有Item slots,直到找到空位置插入
在循环中,每次迭代时检查当前items,若为空,用传入的item填充items数组位置,并改变Image数组,使之显示在屏幕(没有东西时关闭图像),跳出循环
// This function is called by the PickedUpItemReaction in order to add an item to the inventory.
|
Remove:
找到需要删除的项,改为空,并关闭显示
重命名快捷键:F2或ctrl R
// This function is called by the LostItemReaction in order to remove an item from the inventory. |
整合库存到已有代码(integrate inventory)
使用这两个预存脚本
1. Navigate to Scripts > ScriptableObjects > Interaction >
Reactions > DelayedReactions folder
2. Open the LostItemReaction script for editing
1. Navigate to Scripts > ScriptableObjects > Interaction >
Reactions > DelayedReactions folder
2. Open the PickedUpItemReaction script for editing
3. Uncomment all the commented-out code(取消对所有代码的注释)
4. Save the script and return to the editor
LostItemReaction
如拿出硬币操作
public class LostItemReaction : DelayedReaction
{
public Item item; // Item to be removed from the Inventory.
private Inventory inventory; // Reference to the Inventory component.
protected override void SpecificInit()
{
inventory = FindObjectOfType ();
}
protected override void ImmediateReaction()
{
inventory.RemoveItem (item);
}
}
PickedUpItemReaction
public class PickedUpItemReaction : DelayedReaction
{
public Item item; // The item asset to be added to the Inventory.
private Inventory inventory; // Reference to the Inventory component.
protected override void SpecificInit()
{
inventory = FindObjectOfType();
}
protected override void ImmediateReaction()
{
inventory.AddItem(item);
}
}
1. Add an Inventory component to the PersistentCanvas GameObject(在PersistentCanvas添加Inventory脚本组件,这样能够在整个系统持久保存,而不是基于某个场景)
- Note that the Inventory component has two separate and unassociated arrays(这时组件的属性栏中有两个数组)
建立自定义数组,分别关联四个项的images和items数组元素
Runtime与Edit Time表现形式不同,创建Editor。针对每一个对象,需要一个对应表示àserializedObject(序列化对象,运行时对象的通用表示,serializedObject能够在Inventory中看到,得到序列化属性(serializedProperty))
serializedProperty属于serializedObject,并代表对象中的字段。
1. Find the Scripts > Editor > Inventory folder
2. Create a C# script called InventoryEditor
3. Open the InventoryEditor script for editing
using UnityEditor; |
不继承MonoBehaviour ,MonoBehaviour 是要附加到游戏对象的脚本
public class InventoryEditor : Editor |
给出Editor的目标type,CustomEditor,参数为Inventory
[CustomEditor(typeof(Inventory))] |
序列化每一个字段的属性
private bool[] showItemSlots = new bool[Inventory.numItemSlots]; // Whether the GUI for each Item slot is expanded.每个项目槽是否展开显示image组件和item
|
在OnEnable函数找到相应名字的属性
private void OnEnable () |
覆写(override)OnInspectorGUI
目标是Inventory,这里有序列化对象
这些序列化属性不属于inventory,而属于序列化对象
改变属性时,update改变序列化对象
改变序列化对象时,确保变化回到目标对象
public override void OnInspectorGUI () |
ItemSlotGUI:
传入参数,确定是哪个item
每个item显示在一个box中
使用(Vertical layout group)垂直布局组
private void ItemSlotGUI (int index) //使用序列化属性的默认值PropertyField |
1. Observe the change in the Inspector(看属性栏的变化)
2. Drag the ItemImage GameObject from each ItemSlot to the appropriate Image object field on the Inventory inspector(PersistentCanvas中的Inventory脚本组件,分别引用四个槽的images对象)
3. Save the scene
P184
-P204
Build a system allowing the Player to interact with the game
创建交互系统与玩家互动,改变游戏状态(game state)
• Conditions
Create a system to check the current game state
• Scripting Patterns
• Scriptable Objects
• Generic Classes(泛型类)
• Inheritance(继承)
• Extension Methods(扩展方法)
• Reactions
Create a system to perform actions based on condition state
• Polymorphism(多态)
• Further editor scripting
• Serialization(序列化)
• Interactables
Create a system to define what the player can interact with
• Interactable Geometry
• EventSystem
• Interaction System Summary
Create an Interactable GameObject > OnClickEvent > Condition > Reaction
The Interactable GameObject will be:(创建交互式游戏对象,接收用户点击输入作为click event,使用event System)
• stand-alone and hold all logic & events(独立,处理所有逻辑、事件)
• decoupled from the actual props in the scene(与现实场景中的道具无关,但在道具周围设置带有碰撞器的对象,进行互动)实际有两个道具
The Interactable GameObject will have:交互对象将有
• A Collider to detect clicks(检测点击的碰撞器)
• An EventTrigger to process clicks(处理点击的EventTrigger事件触发器)
• An Interactable component to control the logic of the interaction(控制逻辑与交互的交互脚本组件)
玩家走到交互对象处,点击发生交互,条件集合不吻合,则查看另一个条件集合,吻合则调用反应集合:
Conditions are:条件
• Data objects that contain only an identifier and a boolean(保存标识符和bool状态的数据对象)
• Saved as ScriptableObject assets that can be used to compare the state of a Condition(脚本对象assets比较条件状态)
Reactions:反应
• Accommodate a wide variety of possible actions(保存可能的行动(播放动画设置,显示文本,加载场景-动画反应、文本反应、场景反应))
• Use Inheritance and Polymorphism to create specific Reactions for each possible type of action(用继承和多态性创建每种可能类型的特定反应行动)
Reactions will be Encapsulated(反应封装)
每个互动不需要知道将发生什么反应,只需知道是反应。一个单独的反应集合的组件,将分发需反应信息
• The Interactable will have a single object reference to a Reaction(接口将有single object引用Reaction)
• The Interactable will call a single React function regardless of how many actual Reactions there are(互动将调用一个React函数,不管实际有多少反映)
To improve workflow Custom Inspectors will be created to accommodate all of the different types of Reactions, Conditions and Interactables(自定义检查器,创造、容纳,适应所有不同类型的Reactions, Conditions and Interactables)
5:12
Create a system to make Reactions conditional(创建反应条件系统
All Conditions will be ScriptableObjects(所有条件都是脚本对象)
• Some Conditions will be saved as assets to represent the global state of the game(一些条件保存为资源,代表全局游戏状态)
• Some Conditions will be instances in the scene which represent the required state(一些条件是场景中代表所需状态的实例)
条件集合(condition)中有场景中的实例(instance in the scene),
比较他们与其他保存在asset的条件的实例
场景中实例需要发生反应,asset中实例的是游戏全局状态(global state of the game)
比较这两个实例的状态(hash),看是否满足(条件状态与全局状态(可通过玩家改变)相同)
Reaction:
reaction改变状态(state)
Conditions中有全局状态的引用,用满足状态的反应改变asset中的全局状态
•Conditions will have an integer hash representing their description (条件中有一个整数hash)
•Comparing the hash will be more efficient than comparing the descriptions as strings(比较hash比比较字符串的效率高)
The Condition script
ScriptableObject>Interaction>Condition
这次不在菜单中创建asset,而创建场景中实例(instances exist in the scene),为全局状态创建的assets
三个变量
using UnityEngine; //The Conditions that are part of the AllConditions asset are those that are set by //Those that are on ConditionCollections are compared to the |
The AllConditions script
同文件夹下
代表全局游戏状态(玩家是否有硬币等)
继承:
public class AllConditions : ResettableScriptableObject |
AllConditions必须有reset函数,每次打开游戏时,清除旧的游戏状态
变量:
public Condition[] conditions;// All the Conditions that exist in the game.条件数组 |
public static AllConditions Instance// The public accessor for the singleton instance.私有单例的公共访问器(get&set) |
// This function will be called at Start once per run of the game.每次加载游戏时调用 |
// This is called from ConditionCollections when they are being checked by an Interactable that has been clicked on.用来检测条件的静态函数,比较场景inspector设置的条件,ConditionCollections传入需要的条件 |
15:46
ConditionCollection
1.Navigate to Scripts > ScriptableObjects > Interaction > Conditions folder
2.Open the ConditionCollection script for editing
存在interactive的对象,interactable有一个条件数组“condition collections”,每个condition collections有一个条件数组
condition collections必须引用reaction collection,要让所有条件被满足(satisfied)
16:32
public class ConditionCollection : ScriptableObject |
变量:
public string description; // Description of the ConditionCollection. This is used purely for identification in the inspector.在属性栏识别条件是关于什么的 |
函数:
检查条件,做出反应
// This is called by the Interactable one at a time for each of its ConditionCollections until one returns true.由可交互物调用,依次检查每个ConditionCollections,直到返回true |
完整脚本
using UnityEngine;
// This class represents a single outcome from clicking单击
// on an interactable. It has an array of Conditions
// and if they are all met an ReactionCollection that
// will happen.
public class ConditionCollection : ScriptableObject
{
public string description; // Description of the ConditionCollection. This is used purely for identification in the inspector.
public Condition[] requiredConditions = new Condition[0]; // The Conditions that need to be met in order for the ReactionCollection to React.
public ReactionCollection reactionCollection; // Reference to the ReactionCollection that will React should all the Conditions be met.
// This is called by the Interactable one at a time for each of its ConditionCollections until one returns true.
public bool CheckAndReact()
{
// Go through all Conditions...
for (int i = 0; i < requiredConditions.Length; i++)
{
// ... and check them against the AllConditions version of the Condition. If they don't have the same satisfied flag, return false.
if (!AllConditions.CheckCondition (requiredConditions[i]))
return false;
}
// If there is an ReactionCollection assigned, call its React function.
if(reactionCollection)
reactionCollection.React();
// A Reaction happened so return true.
return true;
}
}
The EditorWithSubEditors script
带有子编辑器的编辑器
确保每个放进条件数组的脚本对象是正确的
Interactable中有conditionCollection数组
为interactable创建编辑器,不要那些对象字段,而是能看出conditionCollection中发生了什么,condition中发生了什么(下拉列表,可以选择编辑那个条件)
每个编辑器需要一些子编辑器
创建一个有大部分功能的基类
conditionCollection编辑器将有condition子编辑器
文件位置:scripts>editor>Abstracts
using UnityEngine;
using UnityEditor;//加载Editor命名空间
// This class acts as a base class for Editors that have Editors这个类作为嵌套编辑器的基类抽象类(abstract),继承,不实例化
// nested within them. For example, the InteractableEditor has交互编辑器有一个ConditionCollectionEditors数组
// an array of ConditionCollectionEditors.
// It's generic types represent the type of Editor array that are它的泛型类表明嵌套编辑器的数组类型,和编辑器的目标类型
// nested within this Editor and the target type of those Editors. 泛型TEditor- target editor(如Condition Collection Editor), TTarget-type of Target. 获得ConditionCollectionEditor,ConditionEditor作为其子对象,目标是conditions。Where是对泛型类型的限制 ,TEditor要继承Editor
public abstract class EditorWithSubEditors : Editor
where TEditor : Editor
where TTarget : Object
{
protected TEditor[] subEditors; // Array of Editors nested within this Editor.嵌套子编辑器数组
// This should be called in OnEnable and at the start of OnInspectorGUI. 在OnEnable,OnInspectorGUI开始处被调用,创建子编辑器,传入目标数组(可能是conditions或condition collection)
protected void CheckAndCreateSubEditors (TTarget[] subEditorTargets)
{
// If there are the correct number of subEditors then do nothing.保证子编辑器存在,且数量正确-旧的编辑器已经清空
if (subEditors != null && subEditors.Length == subEditorTargets.Length)
return;
// Otherwise get rid of the editors.
CleanupEditors ();
// Create an array of the subEditor type that is the right length for the targets.为目标创建自编辑器数组
subEditors = new TEditor[subEditorTargets.Length];
// Populate the array and setup each Editor.填充数组,创建编辑器
for (int i = 0; i < subEditors.Length; i++)
{
subEditors[i] = CreateEditor (subEditorTargets[i]) as TEditor;
SubEditorSetup (subEditors[i]);
}
}
// This should be called in OnDisable.
protected void CleanupEditors ()
{
// If there are no subEditors do nothing.
if (subEditors == null)
return;
// Otherwise destroy all the subEditors.销毁所有子编辑器
for (int i = 0; i < subEditors.Length; i++)
{
DestroyImmediate (subEditors[i]);
}
// Null the array so it's GCed.将数组引用清空
subEditors = null;
}
// This must be overridden to provide any setup the subEditor needs when it is first created.第一次创建时重写setup子编辑器,就像构造函数一样
protected abstract void SubEditorSetup (TEditor editor);
}
DestroyImmediate()与Destroy()不同,用于销毁编辑器,Destroy销毁场景或对象
InteractableEditor的序列化对象指向interactable
InteractableEditor有一个子编辑器数组,指向conditionCollection
子编辑器中有ConditionCollection编辑器,每个编辑器有一个序列化对象,指向conditionCollection,它有一个子编辑器数组,指向conditions
每个conditionEditor,序列化对象代表condition
ConditionCollectionEditor
1.Navigate to the Scripts > Editor > Interaction > Conditions folder
2.Select the ConditionCollectionEditor script and openit for editing(文件路径)
序列化对象、属性、编辑器使用
using UnityEngine;
using UnityEditor;
[CustomEditor(typeof(ConditionCollection))]//target
public class ConditionCollectionEditor : EditorWithSubEditors
{
public SerializedProperty collectionsProperty; // Represents the array of ConditionCollections that the target belongs to. Condition序列化属性代表target依附的ConditionCollections数组
private ConditionCollection conditionCollection; // Reference to the target.引用目标,存储条件的引用
private SerializedProperty descriptionProperty; // Represents a string description for the target.代表目标字符串
private SerializedProperty conditionsProperty; // Represents an array of Conditions for the target.代表目标Conditions数组
private SerializedProperty reactionCollectionProperty; // Represents the ReactionCollection that is referenced by the target.代表引用目标的ReactionCollection 数组
//常量
private const float conditionButtonWidth = 30f; // Width of the button for adding a new Condition.添加新条件的按钮宽
private const float collectionButtonWidth = 125f; // Width of the button for removing the target from it's Interactable.从互动删除目标的按钮宽度
private const string conditionCollectionPropDescriptionName = "description";
// Name of the field that represents a string description for the target. 代表目标描述字符串的字段名
private const string conditionCollectionPropRequiredConditionsName = "requiredConditions";
// Name of the field that represents an array of Conditions for the target.代表Conditions目标数组
private const string conditionCollectionPropReactionCollectionName = "reactionCollection";
// Name of the field that represents the ReactionCollection that is referenced by the target.代表被目标引用的Reaction Collection
private void OnEnable ()
{
// Cache a reference to the target.将target转换为条件嘉禾,保存target引用
conditionCollection = (ConditionCollection)target;
// If this Editor exists but isn't targeting anything destroy it.销毁目标指向为空的编辑器
if (target == null)
{
DestroyImmediate (this);
return;
}
// Cache the SerializedProperties.保存序列化属性
descriptionProperty = serializedObject.FindProperty(conditionCollectionPropDescriptionName);
conditionsProperty = serializedObject.FindProperty(conditionCollectionPropRequiredConditionsName);
reactionCollectionProperty = serializedObject.FindProperty(conditionCollectionPropReactionCollectionName);
// Check if the Editors for the Conditions need creating and optionally create them.创建Conditions编辑器需要的属性
CheckAndCreateSubEditors (conditionCollection.requiredConditions);
}
private void OnDisable ()
{
// When this Editor ends, destroy all it's subEditors.销毁条件集合时保证销毁所有子编辑器
CleanupEditors ();
}
// This is called immediately when a subEditor is created.自编辑器创建时调用
protected override void SubEditorSetup (ConditionEditor editor)
{
// Set the editor type so that the correct GUI for Condition is shown.设置编辑器种类,显示Condition界面
editor.editorType = ConditionEditor.EditorType.ConditionCollection;
// Assign the conditions property so that the ConditionEditor can remove its target if necessary.分配条件属性,知道它属于哪个条件数组,使其目标能被删除
editor.conditionsProperty = conditionsProperty;
}
public override void OnInspectorGUI ()
{
// Pull the information from the target into the serializedObject.将信息从目标拉取到序列化对象
serializedObject.Update ();
// Check if the Editors for the Conditions need creating and optionally create them.检查并创建Conditions需要的编辑器
CheckAndCreateSubEditors(conditionCollection.requiredConditions);
EditorGUILayout.BeginVertical(GUI.skin.box);
EditorGUI.indentLevel++;
EditorGUILayout.BeginHorizontal();
// Use the isExpanded bool for the descriptionProperty to store whether the foldout is open or closed.用描述属性的bool变量存储是否展开
descriptionProperty.isExpanded = EditorGUILayout.Foldout(descriptionProperty.isExpanded, descriptionProperty.stringValue);
// Display a button showing 'Remove Collection' which removes the target from the Interactable when clicked.显示按钮,点击删除Interactable的目标条件 序列化属性不知道他们是什么属性的,有所有种类属性,正在使用的不能被其他地方使用
if (GUILayout.Button("Remove Collection", GUILayout.Width(collectionButtonWidth)))
{
collectionsProperty.RemoveFromObjectArray (conditionCollection);//需要从数组中删除的对象,Unity没有单独删除特定类型对象的具体数组,需要扩展方法(如下脚本)扩展方法已经知道传递那个序列化属性,所以只需要一个参数
}
EditorGUILayout.EndHorizontal();
// If the foldout is open show the expanded GUI.展开则显示界面
if (descriptionProperty.isExpanded)
{
ExpandedGUI ();
}
EditorGUI.indentLevel--;
EditorGUILayout.EndVertical();
// Push all changes made on the serializedObject back to the target.所有序列化对象中的变化返回目标
serializedObject.ApplyModifiedProperties();
}
private void ExpandedGUI ()
{
EditorGUILayout.Space();
// Display the description for editing.展示编辑的描述
EditorGUILayout.PropertyField(descriptionProperty);
EditorGUILayout.Space();
// Display the Labels for the Conditions evenly split over the width of the inspector. 均匀宽度展开Conditions标签
float space = EditorGUIUtility.currentViewWidth / 3f;
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("Condition", GUILayout.Width(space));
EditorGUILayout.LabelField("Satisfied?", GUILayout.Width(space));
EditorGUILayout.LabelField("Add/Remove", GUILayout.Width(space));
EditorGUILayout.EndHorizontal();
// Display each of the Conditions.展示每个条件
EditorGUILayout.BeginVertical(GUI.skin.box);
for (int i = 0; i < subEditors.Length; i++)
{
subEditors[i].OnInspectorGUI();
}
EditorGUILayout.EndHorizontal();
// Display a right aligned button which when clicked adds a Condition to the array.点击添加条件进入数组时,展示一行按钮
EditorGUILayout.BeginHorizontal();
GUILayout.FlexibleSpace ();
if (GUILayout.Button("+", GUILayout.Width(conditionButtonWidth)))
{
Condition newCondition = ConditionEditor.CreateCondition();
conditionsProperty.AddToObjectArray(newCondition);
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space();
// Display the reference to the ReactionCollection for editing.展示编辑ReactionCollection引用
EditorGUILayout.PropertyField(reactionCollectionProperty);
}
// This function is static such that it can be called without an editor being instanced.静态方法(可直接调用)
public static ConditionCollection CreateConditionCollection()
{
// Create a new instance of ConditionCollection.创建ConditionCollection实例
ConditionCollection newConditionCollection = CreateInstance();
// Give it a default description.初始化描述
newConditionCollection.description = "New condition collection";
// Give it a single default Condition.假设人们创建时想至少创建一个条件,初始化默认条件但未填充,仍为null
newConditionCollection.requiredConditions = new Condition[1];
newConditionCollection.requiredConditions[0] = ConditionEditor.CreateCondition();//使用静态公共方法,创建条件
return newConditionCollection;
}
}
The RemoveFromObjectArray extension method of the SerializedPropertyExtensions script
在不同的脚本中保存扩展方法
序列化属性扩展脚本
SerializedPropertyExtensions
文件位置:Extensions
静态类可以被用在任何地方,所有扩展方法都是静态
using UnityEngine;
using UnityEditor;
// This class contains extension methods for the SerializedProperty这个类包含SerializedProperty类的扩展方法
// class. Specifically, methods for dealing with object arrays.处理对象数组方法
public static class SerializedPropertyExtensions
{
// Use this to add an object to an object array represented by a SerializedProperty.
public static void AddToObjectArray (this SerializedProperty arrayProperty, T elementToAdd)
where T : Object
{
// If the SerializedProperty this is being called from is not an array, throw an exception.
if (!arrayProperty.isArray)
throw new UnityException("SerializedProperty " + arrayProperty.name + " is not an array.");
// Pull all the information from the target of the serializedObject.
arrayProperty.serializedObject.Update();
// Add a null array element to the end of the array then populate it with the object parameter.
arrayProperty.InsertArrayElementAtIndex(arrayProperty.arraySize);
arrayProperty.GetArrayElementAtIndex(arrayProperty.arraySize - 1).objectReferenceValue = elementToAdd;
// Push all the information on the serializedObject back to the target.
arrayProperty.serializedObject.ApplyModifiedProperties();
}
// Use this to remove the object at an index from an object array represented by a SerializedProperty.
public static void RemoveFromObjectArrayAt (this SerializedProperty arrayProperty, int index)
{
// If the index is not appropriate or the serializedProperty this is being called from is not an array, throw an exception.
if(index < 0)
throw new UnityException("SerializedProperty " + arrayProperty.name + " cannot have negative elements removed.");
if (!arrayProperty.isArray)
throw new UnityException("SerializedProperty " + arrayProperty.name + " is not an array.");
if(index > arrayProperty.arraySize - 1)
throw new UnityException("SerializedProperty " + arrayProperty.name + " has only " + arrayProperty.arraySize + " elements so element " + index + " cannot be removed.");
// Pull all the information from the target of the serializedObject.
arrayProperty.serializedObject.Update();
// If there is a non-null element at the index, null it.
if (arrayProperty.GetArrayElementAtIndex(index).objectReferenceValue)
arrayProperty.DeleteArrayElementAtIndex(index);
// Delete the null element from the array at the index.
arrayProperty.DeleteArrayElementAtIndex(index);
// Push all the information on the serializedObject back to the target.
arrayProperty.serializedObject.ApplyModifiedProperties();
}
// Use this to remove an object from an object array represented by a SerializedProperty.从对象数组中删除,public,能从序列化属性外部调用,通用方法,使用泛型(限制为对象)。扩展方法需要第一个参数,在这个SerializedProperty调用这个方法
public static void RemoveFromObjectArray (this SerializedProperty arrayProperty, T elementToRemove)
where T : Object
{
// If either the serializedProperty doesn't represent an array or the element is null, throw an exception.如果serializedProperty不是数组,或元素是空,抛出异常
if (!arrayProperty.isArray)
throw new UnityException("SerializedProperty " + arrayProperty.name + " is not an array.");
if(!elementToRemove)
throw new UnityException("Removing a null element is not supported using this method.");
// Pull all the information from the target of the serializedObject.从serializedObject的target拉取所有信息
arrayProperty.serializedObject.Update();
// Go through all the elements in the serializedProperty's array...找到每个序列化属性元素
for (int i = 0; i < arrayProperty.arraySize; i++)
{
SerializedProperty elementProperty = arrayProperty.GetArrayElementAtIndex(i);
// ... until the element matches the parameter...
if (elementProperty.objectReferenceValue == elementToRemove)
{
// ... then remove it.
arrayProperty.RemoveFromObjectArrayAt (i);
return;
}
}
//没有找到数组元素
throw new UnityException("Element " + elementToRemove.name + "was not found in property " + arrayProperty.name);
}
}
53:42
1.Navigate the Scenes folder in the Project window 返回场景文件夹
2.Open the SecurityRoom scene 打开SecurityRoom场景
3.Select the LaserGridInteractable GameObject选择LaserGridInteractable游戏对象
4.On the Interactable component, click the Add Collectionbutton 在Interactable脚本组件,点添加集合按钮
5.Set the Description of the new condition collection to LaserGridOff设置新条件集合的Description为LaserGridOff
1.From the dropdown, set the Condition to LasersDeactivated下拉列表给LaserGridOff设置条件
2.Check the Satisfied box选择满足框
3.Expand the LaserGridInteractable展开LaserGridInteractable
4.Drag the BeamsOffReaction from the hierarchy onto the InteractableReactionfield for the Condition Collection从层级窗口拖拽BeamsOffReaction到InteractableReactionfield条件集合
创建条件集合,添加条件,条件满足时使用反应集合-BeamsOffReaction
P162
Brief:
Create a system that performs a series of actions based on Conditions and the current Game State when the Player Clicks on an Interactable创建一个能在玩家点击时,基于条件和游戏状态播放一系列动画的系统
There can be many different types of Reactions such as:(不同的反应类型)
• Play an animation播放动画
• Play a sound声音
• Display text on screen在屏幕上显示文本
• Add the item to the inventory将物料添加到库存
• Disable a GameObject in the scene禁用场景中的游戏对象
• Change a global condition to acknowledge the pickup更改全局条件以确认拾取
Depending on type, some Reactions must have an optional delay while others must be instant(根据类型的不同, 某些反应必须具有可选的延迟, 而其他的必须是即时的)
Each type of Reaction will need its own Custom Editor每种类型的反应需要自定义编辑器
Approach:
• Collections of Reactions will be encapsulated, initialized and called as part of a ReactionCollection script which contains a single public React function"反应集合" 将被封装、初始化,并作为包含单个公共反应函数的ReactionCollection脚本的一部分调用
Individual Reactions will all be different classes inheriting from a base Reaction class as this will maintain the key functionality across all the variations of Reactions and allow them to be treated as Reactions due to Polymorphism单个反应都是继承相同基反应类的不同类, 因为这将保持所有反应变体中的关键功能, 并允许它们被视为多态性引起的反应
Reactions will derive from ScriptableObject which allows Polymorphic Serialization so that custom inspectors can can be created based on the type of the Reaction反应将从脚本对象派生, 该对象允许多态体序列化, 因此可以根据反应的类型创建自定义检查器
路径:MonoBehaviors/interaction
MonoBehavior将附加到游戏对象
using UnityEngine; |
多态
Scriptable Objects and Polymorphic Serialization
Reaction
public Reaction[] reactions; // TextReaction, AudioReaction, AnimationReaction在同一空间存储所有种类反应,在inspector都被视作reaction。看inspector时,反应集合只把他们当做reaction
Reaction : ScriptableObject
public Reaction[] reactions; // TextReaction, AudioReaction, AnimationReaction在脚本对象,展示正确名字。所有要把反应写成脚本对象
序列化serialization存储信息到磁盘(disk)
非多态序列化,只会看到默认反应,多态序列化能看到正确反应
Return to ReactionCollection script
得到游戏对象,希望它的组件
1. Navigate to the Scripts > Editor > Interaction
2. Find the ReactionCollectionEditor自定义编辑器
3. Open the ReactionCollectionEditor script for editing
4. This is the finished custom editor we will be working on:
using System;
|
1. Create a new Empty GameObject创建测试对象
2. Add an ReactionCollection component添加反应集合组件
3. Navigate to Scripts > ScriptableObjects > Interaction > Reactions folder
4. Drag either a DelayedReactions or ImmediateReactions onto the Drop area of the ReactionCollection component在反应文件夹添加脚本对象到拖拽框
1. Delete the test GameObject you just made
创建特定反应类型
TextReaction
1. Navigate to the Scripts > ScriptableObjects > Interaction > Reactions > ImmediateReactions folder.
2. Create a new C# script
3. Name it TextReaction
4. Open the script for editing
using UnityEngine; |
TextReactionEditor
1. Navigate to the Scripts > Editor > Interaction > ReactionEditors folder.
2. Create a new C# script
3. Name it TextReactionEditor
4. Open the TextReactionEditor script for editing
message多行编辑器
using UnityEditor; |
1. Open the SecurityRoom scene
2. In the Hierarchy, find the DefaultReaction child of the PictureInteractable
3. Add a TextReaction using either the dropdown or the drop area下拉列表添加文字反应
4. Set the Message of the Text Reaction to “He looks pretty trustworthy.”设置文字
5. Save the scene
P301
Create an Interactable system to process Player input and tie Conditions and Reactions together创建一个可交互系统来处理玩家输入, 并将条件和反应捆绑在一起
When an Interactable is clicked on the Player must move to a specific location in the scene marked by a Transform reference called InteractionLocation当单击 Interactable 时, 玩家必须移动到场景中 "InteractionLocation" 的位置(Transform)引用标记的特定位置
Upon reaching the InteractionLocation the player must call a public Interact function which will in turn call an appropriate Reaction collection based on a check of Conditions and Game State到达交互位置后, 玩家必须调用public Interact函数, 该函数将根据条件和游戏状态调用适当的反应集合
Create a custom inspector for the Interactable to make it easier to understand自定义Interactable的inspector
The Interactable will have:
• An EventTrigger component to registers clicks用于接收点击的事件触发器组件
• A Box Collider to define the volume to interact with一个Box碰撞器, 用于定义interact的体积
• When clicked, the Interactable sets the current destination for the Player单击时, Interactable设置玩家的目的地
• When the Player arrives, the Interact function will be called by the PlayerMovement script当玩家到达时, Interact函数将由PlayerMovement脚本调用
Create a Custom Inspector for the Interactable component自定义Interactable组件的inspector
1. Navigate to the Scenes folder
2. Open the SecurityRoom scene
The Event System
1. Send events发送事件
1. Physics Raycaster component投射器组件连接到camera
2. Receive events接收事件
1. Colliders & Event Triggers碰撞器、事件触发器
3. Manage events管理事件
1. The Event System事件系统
To recap the structure of Interactables(Interactables结构)
Interactable中有条件集合,每个条件集合中有条件,并联系到满足条件后发生的反应集合,如果没有条件集合被满足,则发生interactable的默认条件集合
守卫的互动:
using System.Collections; |
重温,替代普通编辑器,而不是继承有子编辑器的编辑器。已经获取target数组,需要子编辑器,有一个条件集合数组,每个条件集合有一个编辑器,想有一个interactive带有条件集合数组,需要一个interactive编辑器带有条件集合编辑器数组
Interactable编辑器is targeting interactable,序列化对象代表interactable, 子编辑器代表条件集合
每个条件集合有条件集合编辑器,他们的序列化对象代表条件集合
子编辑器数组代表条件
1. On the Pointer Click event of the EventTrigger, drag on the Player GameObject将玩家对象拖到EventTrigger组件的Pointer Click事件处
2. From the function dropdown select PlayerMovement > OnInteractableClick选择方法
3. Drag the Interactable component onto the object parameter field拖拽Interactable组件到参数栏
1. Select the InteractionLocation child GameObject设置
2. Set its position to -1.5, 0, 0
3. Set its rotation to 0, 90, 0
1. Select the DefaultReaction child GameObject
2. Add a TextReaction and an AudioReaction to the ReactionCollection component在ReactionCollection组件上通过下拉列表选择添加TextReaction和AudioReaction反应
3. Set the Message of the Text Reaction to “He looks pretty trustworthy.”设置文字反应的消息
4. Set the Audio Source of the Audio Reaction to VO设置声音反应的音源
5. Set the Audio Clip of the Audio Reaction to PlayerThisGuyLooksTrustworthy和音频片段
1. Save the scene
2. Open the Persistent scene
3. Test
4. Exit Play Mode!
P312-349
Create a system to transition between scenes创建一个系统能够在场景之间转换
• Important information must be retained between scenes (eg: inventory state) 必须在场景之间保留重要信息 (例如: 库存状态)
• Fade to black during scene transitions在场景过渡过程中淡入黑色
• Multiple spawn points need to be supported due to possible entries from multiple other scenes由于从多个其他场景进入, 因此需要支持多个生成点
Create a system to save data between scene loads and unloads创建一个系统, 在场景加载和卸载之间保存数据
• Upon unloading a scene information such as the position of a GameObject must be stored so that on returning to the scene the information can be used again卸载场景信息 (如游戏对象的位置) 时, 必须存储, 以便在返回场景时可以再次使用该信息
The project architecture will be based on a main, or “persistent”, scene that will stay loaded all times项目架构将基于一个主场景, 或 "持久" 场景, 将始终保持加载状态
This “persistent” scene will handle the loading and unloading of other scenes这个 "持久" 的场景将处理其他场景的装卸
Certain information that needs to persist throughout all scenes (eg: the inventory) can be stored in the “persistent” scene某些需要在所有场景中保留的信息 (例如: 库存) 可以存储在 "持久" 场景中
The Persistent scene is not suitable to store all information, such as details about GameObjects that exist only in one particular scene (eg: the bird) 持久场景不适合存储所有信息, 例如仅存在于一个特定场景中的有关游戏对象的详细信息 (例如: 鸟)
ScriptableObject assets will be used to temporarily store this data so that it can be loaded again on return to a scene将使用脚本对象assets暂存此数据, 以便在返回到场景时可以再次加载这些数据
场景1有九个抽象矩形,游戏时改变一些(如位置),保存相关数据,转化为asset
回到该场景时,加载默认数据,然后从asset重新加载reload变化的数据
场景控制架构
场景1变黑,unload,加载场景2
例子:
用脚本对象(scriptObject)存储数据(temporary、only runtime不接受会话之间的存储)
只能为存储运行时文件(runtime files)
委托/事件和lambda表达式
基类Action是特定类型的委托, 表示返回值为 void 且没有参数的函数
Functions are stored in delegates as follows: 函数存储在委托中
void MyFunction () { }
Action myAction = MyFunction;
The functions stored in delegates can be called as follows
myAction();
1. Actions also come in generic types 一般类型
Action
2. The above Action can store a function that returns void and takes in a single integer parameter as below 上面的 action 可以存储一个返回 void的函数, 并接受一个整数参数, 如下所示
public void MyFunction (int myInt) { }
Action
public void MyFunction (Action myActionFunction) { }
1. A delegate can be subscribed to by using the += operator and unsubscribed from using the -= operator 可以使用 + = 运算符添加委托挂接, 也可以使用-= 运算符取消委托挂接
myAction += MyFunction;
myAction -= MyFunction;
当有巨大巨慢的函数要传递,每次传递时这个函数都会被调用,用委托传递时,它的父函数可以决定是否调用委托函数。
A delegate can be used as an event by putting the keyword 'event' in its definition 委托可以通过将关键字 "event" 放在其定义中用作事件
public event Action myEventAction;
1. It is very important that once an event has been subscribed to, that it is unsubscribed from before the object is destroyed 非常重要的是, 一旦一个事件被挂接, 它就会在对象被销毁之前取消挂接
2. Events that are still subscribed to will not be Garbage Collected and will therefore cause memory leaks if they are left subscribed仍挂接的事件将不会被收集垃圾, 因此, 如果它们保留挂接, 则会导致内存泄漏
--event一旦挂接(代表有可用的调用),一定要取消挂接
如:获取scene controller,将要卸载场景,它需要卸载所有场景有关物,存储场景想存储的数据。
实现方法:1.scene controller可以引用每一个想要存储的东西,告诉每一个东西场景要结束了该存储了。2.场景结束时调用event存储数据,它挂接了那些东西。可以每次添加新东西
1. Calling events works the same as calling delegates, this will call all of the functions which have subscribed to the event 调用事件的工作方式与调用委托相同, 这将调用挂接事件的所有函数
public event Action myEventAction;
myEventAction += MyFunction;
myEventAction();
1. The key point about events is that the triggering class does not need to tell all concerned classes to call a specific function事件的关键点是, 触发类不需要让所有相关类调用特定的函数
2. Instead, concerned classes can listen for an event and then act accordingly相反, 有关类可以监听一个事件, 然后采取相应的行动
每次添加新东西,可以拖到inspector引用它,写一条调用线(calling line)将要加载到场景。
这时挂接和取消挂接比较容易,卸载当前场景的东西时可以直接取消挂接这个事件。
就像在推特订阅某个人,当他发布了消息,就将被通知。
2. They have the following syntax: 'variable names' => 'expression(s) using those variables'它们具有以下语法: "变量名" = > "使用这些变量的表达式"
1. The variables declared for a lambda often very short 没有声明
2. Variables are usually implicitly typed, for example: 通常用匿名类(implicitly typed)
x => x == someValue
3. In the above case, x implicitly has the same type as someValue
1.Lambda expressions are generally are quite short 2. Here are some examples of delegates using Lambda Expressions一些委托使用lambda表达式的例子
Action myAction = () => Debug.Log("something");
Action
One key reason to use lambda expressions is to simplify our code使用lambda表达式的原因是简化代码
This:
Action
private void SomeFunction (string name) { print(name); }
Becomes this: Action
注意闭合大括号后还有一个分号
15:25
P404
1. Navigate to the Scripts > MonoBehaviours > SceneControl
2. Open the SceneController script for editing
using System; |
1. Navigate to the Scripts > ScriptableObjects > DataPersistence folder
2. Open the SaveData script for editing
FindIndex的等价函数
private int GetIndex (string key) {
for (int i = 0; i < keys.Count; i++) {
if (keys[i] == key)
return i;
}
return -1; }
using System; |
路径:MonoBehaviours/DataPersistance/Saver
1. Open the Persistent scene 2. Test by entering Play Mode 3. Exit Play Mode
需要连接要存储信息的游戏对象与save data
using UnityEngine;
|
positionSaver(在同文件夹下)
using UnityEngine;
|
51:10
P229
在写for后按两下Tab,能自动出完整循环
重命名快捷键:F2(VS中为ctrl R)
联系3C店wifi(onlinebj2.4G)密码onlinebj123