这一Phase要实现在SecurityRoom里点击地板Player在上面随着鼠标点击走动的效果。
----------------
点击Hierarchy下的SecurityRoomEnvironment里的指定选项,勾选Static,因为NavMash必须在静态的环境上Bake然后生效。
Window→Navigation→Bake按如下选项
各选项意义见下:
Hierarchy下新建UI→EventSystem,并给EventSystem加上Audio Listener组件。注意,完整的游戏里EventSystem都是建在Presistent Scene里的,这里只是为了检测该单元而建立,之后会删掉。
接着在Hierarchy下的Camera添加组件Physics Raycaster,该组件将对Scene里的物体进行光线投射,并把消息发送到(send)到EventSystem
接着在SecurityRoom上添加添加Mesh Collider组件,Mesh选存好的方案。接着添加Event Trigger组件,点+添加新事件,内容暂时为空(截图用的是完整游戏)
这里介绍一下EventSystem,它分为三步骤:发送(Send events),接收(Receive events)和执行(Manage events)。关系如下:
上面Camera上挂Physics Raycaster组件是为了Send,刚才的SecurityRoom上挂Ecent Trigger组件是为了Receive。
----------------------
在Project View下选择Animators文件夹→Player文件夹下新建Animator Controller命名为ClickToMove。Unity里Animator和Animation都是动画插件,Animator主要为了动画直接的切换,和什么情况下播什么动画;Animation主要控制一个动画的播放内容,比如先哪一帧后一帧。
双击ClickToMove点+新建float型参数命名为Speed,再建4个trigger分别命名为HighTake,MedTake,LowTake,AttemptTake。
在中间的操作区Base Layer上点击右键→Create State→From New Blend Tree创建一个新的混合树(Blend Tree)并命名为Walking。
双击Walking混合树,在Inspector里改名为WalkingBlendTree,点击+选择Add Motion Field。共添加3个三个Motion Fields,然后按名字添加3个对应的状态动画。不要勾选Automate Thresholds(自动化阈值),然后点击Compute Thresholds选择Speed,可以看到3个Motion后的阈值比不再是1:0.5:1而是0:1.5:5.6了。
点击Base Layer回到操作界面,在Animations文件夹→Player下多选Idle,TakeObjectAttempt和TakeObjects动画放入操作界面,并放置成这样
再在这些动画间Make Transition如下图
点击Walking→PlayerIdle的指向线,取消勾选Has Exit Time。点+添加条件,自动出来Speed,选择Less,填0.1,即速度小于0.1时播放Idle动画。再点击PlayerIdle→Walking指向性,同样取消存在时间,添加条件选Greater,填0.1
再对Any State对其他动画的切换条件上加上分别对应其名字的trigger。
注意四个动画到Walking的transition没有设置条件是因为这四个都有默认的Has Exit Time,即这些条件动画播放完了就回到走路状态。
点击PlayerIdle,tag改为Locomotion。再点击Walking,tag改为Locomotion,注意拼写大小写完全正确。这是为了之后在代码里区分动画状态,只有当Locomotion下才能Walking其他的会播放其他动画。
回到Scene视图,从Models文件夹把Player拽到Hierarchy下,选择Layer为Characters,弹出对话框选择覆盖子对象。Position设为(-0.7, 0, 3.5)Rotation设为(0,180,0)刚好在门口对着保镖。Animator组件的Controller选ClickToMove,添加组件Nav Mesh Agent更改如下选项。
Nav Mesh Agent对应属性
最后将Player保存到Prefabs文件夹。
-------------------
Assets→Scripts→MonoBevaviours→Player新建C#命名为PlayerMovenments,并把该脚本拖拽到Player预制上,打开并编辑
首先我们要确定Player的移动方式:先由NavMeshAgent导航到鼠标点击点附近,进入一个范围,降低速度并用Movetowards移动,最后移动到离destination非常近的时候,直接设置Player的position为destinationPosition。
保存后回到Unity,打开选中prefab下的player把引用拖进去,然后点击Hierarchy下的SecurityRoom,把引用拖进去并选好函数。
保存场景测试,鼠标点击地面Player会跟着走动了。
接着编辑PlayerMovement,加上对互动物的反应包括player转向面对互动物。
至于Interactable类,将在之后讲解,回到Unity,点击Hierarchy下的PictureInteractable下的EventTrigger组件将Player拖入并选好函数,接着把Interactable代码组件拖进去。
至此,我们实现了Player在安全屋内走动,点击墙上的画Player会走到画前,并互动。
之后我们将创建一个背包系统。
这个系统首先应在场景切换时保持原内容;这个背包系统应该足够简单,但当背包系统设计变化时可扩展
打开Persistent Scene,点击改为2D模式,16:9宽度
双击Hierarchy下的PersistentCanvas,新建空子对象命名为Inventory,把Inventory拖到FadeImage的上方确保FadeImage可以在切换场景时盖住背包。Inventory下新建空对象ItremSlot,ItemSlot下新建Image,并复制,第一个Image命名为BackgroundImage,SourceImage选为InventorySlotBG。
第二个命名为ItemImage,取消勾选其Image组件,将在用时再添加。
之后把ItemSlot拽到Prefabs使成预制件,再在Hierarchy下的ItemSlot复制3次,共有4个,分别改名为ItemSlot0到ItemSlot3。
选择Inventory添加Vertical Layout Group组件。Inventory的Width改为135,Height改为600。修改Anchor为middle & right(不摁alt或shift),然后修改Pos X为-95。
Assets→Scripts→ScriptableObjects→Inventory下有已写好的Item定义脚本,如下
在Scripts→MonoBehaviour→Inventory下新建Inventory脚本(添加装备&删除装备),打开并编辑
然后把该脚本拖拽到Hierarchy下的PersistentCanvas,其实该脚本可以放在任何一个在该场景的gameobject上,但必须存在于此Scene里。
然后打开Scripts→ScriptableObjects→Interaction→Reactions→DelayedReactions下的LostItemReaction和PickedUpReaction脚本,恢复引用Inventory脚本的语句。这是为了装备置换和捡起时把装备加到装备栏。
之后在Scripts→Editor→Inventory下新建脚本InventoryEditor,打开并编辑
该脚本是把Inventory脚本序列化了,没有该序列化脚本的情况下Inventory脚本组件将显示为:
序列化后显示为
该序列化脚本的作用不仅仅使Inventory脚本组件在Inspector上整齐了,还把Item和IemImage的索引一 一对应了。
把对应的ItemImage拖入。
保存后,点击测试到Market场景里发现Player可捡装备到装备栏并置换了。
怎样添加Item,菜单栏Assets/Creat下有Item选项
会在Assets文件夹下的Item文件夹里新建一个Item我们可以改名,并拖入它所对应的sprite。
The Phase将创建一个系统来检查当前的游戏状态。
一个互动的游戏对象将包括
Condition的形式将包括
这一阶段的代码都在Package里写好。第一个脚本Scripts→ScriptableObjects→Interaction→Conditions→Condition脚本
第二个脚本是同文件夹下的AllConditions
注意到该脚本是继承ResettableScriptableObject的,如下
接着第三个脚本,同文件夹下的ConditionCollection,这个是需要我们编辑的脚本,如下
接下来我们写一个可拥有子编辑器的基础编辑器,Scripts→Editor→Abstracts下EditorWithSubEditors脚本
该基础编辑器将被继承改写,它应该会做到
打开Scripts→Editor→Interaction→Conditions下的ConditionCollectionEditor脚本并编辑
保存后回到Unity,在SecurityRoom场景里,点击Hierarchy下的LaserGridInteractable游戏对象,在Interactable组件上,点击Add Collection按钮。填入对应的选项。并把子对象BeamsOffReaction拖入。
我们上面写的一堆脚本都是为了让这个Interactable在Inspector上体现出这种层次并和游戏数据一 一对应。包括添加和移除ConditionCollection,ConditionCollection下添加和移除Condition,添加新的Reaction等等。
将创建一个系统,当玩家点击一个Interactable对象时,可根据条件和当前游戏状态执行一系列的操作。
Reactions可能包括如下
根据Reaction的不同类型,有些反应必须有可选的延迟,而另一些必须为即时反应(比如掉出Coin,捡起后再说Oh A coin!)我们还会对Reaction做Inspector的序列化Editor
所有Reaction将被封装,初始化进一个ReactionCollection脚本(包含一个public React 方法)并作为该脚本的一部分被调用。
多态性(polymorphism)写一个基类,比如Roguelike的MovingObject,包含了该类的所有基础功能,然后有若干子类继承并改写,这些子类虽然表现不同,但他们都可归为一类。比如Enemy下有Orc,有Goblin。Goblin和Orc表现形式不同,但他们都继承了Enemy,这种通过基类子类把不同的东西提取相同属性而后加上特有属性,且他们都被归为一类来管理的方式就叫多态性。
当游戏规模变大时,系统反应(比如更多的技能和效果,功能等)会更多,我们不能一个个的重写加上去。比如Player攻击狼人,我们新加了角色吸血鬼,难道在Player里再写入一次攻击吸血鬼吗?我们将新建一个基类Enemy,Player攻击Enemy,新加的角色都继承Enemy。它们各自的特性不同,但共性是被攻击。
在此游戏里,Reaction的具体内容(text reaction,audio reaction...)虽然不同,但共性是被鼠标点击了就执行具体内容,所以把Reaction做成一个父类(共性是预加载内容,并执行各种反应内容)。ReactionCollection则只需对父类的Reaction操作,而无需我们新写一个Reaction就要在ReactionCollection脚本里添加一次其类型。
打开Scripts→Monobehaviours→Interaction下的ReactionCollection脚本
Reaction类将继承自ScriptableObject,它的作用就是我们在ReactionCollection的Inspector面板上明确的知道该Reaction的属性,是audio还是text之类。这样在游戏有很多Reaction时不至于拖拽管理混乱。
接着我们把ReactionCollection脚本序列化,打开Scripts→Editor→Interaction下的ReactionCollectionEditor脚本。
回到Unity,我们随便在某个场景,比如Market上新建一个空对象来测试,在该空对象上添加ReactionCollection脚本组件
会出现这样的Inspector
展开Scripts→ScriptableObjects→Interactions→Reactions文件夹,随便选两个拖到“Drop new Reactions here”可以看到已有的Reaction添加进来了,我们也可以摁右边的“-”来去除该Reaction的引用。我们还可以选择想要的Reaction type,选好后点击“Add Selected Reaction”就添加了新的该类型的Reaction。
展开添加的Reaction我们看到添加对应的具体内容。、
测试完记得删掉刚创建的空对象
Scripts→ScriptableObjects→Interaction→Reactions→ImmediateReactions 文件夹下新建脚本命名为TextReacion,打开并编辑
接下来我们把上面的TextReaction序列化,在Scripts→Editor→Interaction→Reactions文件夹下新建脚本命名为TextReactionEditor,打开并编辑
回到Unity,打开SecurityRoom Scene,在Hierarchy下找到PictureInteractable下的子对象DefaultReaction。它上面有一个ReactionCollection组件我们可以把TextReaction(而不是TextReactionEditor)拖拽进去【或者我们可以选择reaction type然后点Add Selected Reaction】,并改写Message为“He looks pretty trustworthy.”
保存场景测试,点击墙上的图片后屏幕下方出现台词。
有了Reaction(反应),有了发生反应的条件(Condition),接下来就可以开始互动了。该阶段将继续创建互动系统,侧重于互动。我们将创建一个系统来定义玩家可与之交互的内容,包括可交互的几何体,使用EventSystem并给出交互系统摘要。
一个Interactable System将处理玩家的输入,并把对应的Condition和Reaction捆绑在一起。当一个Interactable对象被点击时,Player将移动到场景里一个我们已经标记的好的特殊位置InteractableLocation。移动到InteractableLocation后Player将调用一个public Interact方法,然后根据Conditions和当前game state(游戏状态)调用合适的ReactionCollection。
序列化后的Interactable就像下面这样
Interactable序列化的形式如下
就比如Guard有两个互动状态,当有Glasses & Coffee时,台词为Hi Frank,然后Laser可通过。当只有Glasses时,为Hi Frank you can't access without boss's coffee。Laser不可通过。如果这两种ConditionCollection都不满足时,只有default 反应(Get lost)。
Scripts→MonoBehaviours→Interaction文件夹下新建脚本命名为Interactable,打开并编辑。
因为当Player走到指定点时要调用上面的Interact方法,所以打开PlayerMovement脚本,添加如下语句
接下来把上面的Interactable脚本序列化Editor,打开Scripts→Editor→Interaction文件夹下的InteractableEditor脚本
保存后回到Unity,打开SecurityRoom Scene,Hierarchy下新建空对象命名为Interactable,添加Box Collider和Event Trigger组件,然后添加同名脚本作为其组件。Event Trigger上新加一个Event Type选择为Pointer Click。在新建的该事件类下点“+”添加新事件。
再在新建的该空对象上添加两个子空对象,分别命名为InteractableLocation和DefaultReaction。
选中DefaultReaction子对象,添加ReactionCollection脚本为其组件。
回到Interactable对象上,把InteractableLocation和DefaultReaction子对象拖到对应的位置。
这样,一个初始化的基础Interactable对象设置好了,把该对象拖到Prefabs成预制件。
然后把刚创建的Interactable对象改名为PictureInteractable(也可以先删除Interactable,然后从Prefabs里拖出来一个改名)。修改Positon和BoxCollider的Center & Size。
设置好上面的EventTrigger,拖入Player对象,选择PlayerMovement上的OnInteractableClick方法,然后把Interactable脚本组件拖入
子对象InteractableLocation设为如下
接着设置DefaultReaction
注意PictureInteractable没有特定的ConditionCollection,每次互动都播放刚设置的默认反应。
其他的Interactable设置,DefaultReaction设置,对应的ConditionCollection设置,AudioReaction等等都是一样的道理和操作。如有需要可在完成项目里看对应代码。
这一阶段学习如何用Scene Manager创建一个系统来加载场景,和保存当前游戏状态。ScriptableObjects作为临时runtime data存储,删除和lambda表达式(匿名表达式)。
这个将要创建的系统必须:
1.重要的信息(比如inventory 的状态)必须在场景切换时保留。
2.场景切换时由淡出淡入的效果。
3.支持多个spwan points,因为可能有很多不同场景切换进此场景(比如a和c场景,切到b场景player出现的点不一样)。
4.在卸载场景(unloading a scene )时,有些信息例如GameObject的位置必须被存储,以便在返回该场景时可以再次使用信息。(比如我们如果已经捡起了Coin,就存储信息,以免那里又有一个coin出现)
1.我们有一个Persistent Scene一直stay loaded at all times。Persistent Scene 将处理loading & unloading 其他场景。需要在所有场景中持续存在的信息比如Inventory就可以存在Persistent Scene里。
2.Persistent Scene并不适合存一切数据,比如一些游戏对象的细节仅存在在某一个特定的场景(比如the bird)。这时候我们用ScriptableObject assets临时性的存这些数据,这样这些特定数据可以loaded again或return to a scene。
3.场景加载架构:
包括Scene1淡出,Scene2加深fade in
4.save data 架构
比如场景里我们想存Coin的状态。将用一个GameObjectActivitySaver脚本存作一个Scriptable object名为MarketSaveData。
注意,ScriptableObject存数据是temporary(临时的)和runtime only(仅在运行时),它不支持store data between sessions,因为一系列我听不懂的原因^_^。
delegate是一种变量,它可以存储方法而不是data(如bool,int,string)。The basic Action是一个特定类型的Delegate,代表一个没有返回类型和参数的方法。
Action也可以用作一般类型的delegate,下面的Action就返回空且有一个int 参数
delegate最主要的用处,是他们存方法就像变量(variables)一样,所以delegate可以把方法,像传送变量一样,传送到另一个方法去。
例如我们写了一个又长又慢的方法,当有必要时才调用,这时它作为一个方法传递出去并调用是不合算的(每次传进去并调用,将会有一个超长超慢的调用过程)。
所以我们通过delegate来传递该方法,由该方法的父方法决定是否要调用该方法。
你可以通过用 += 或 -=操作符来订阅或取消订阅delegate。这意味着可通过一个delegate添加多个函数,当调用该delegate时将调用多个函数。
注意这里我们所说的Events和Unity里的Event System无关,它只是独立的C#内的脚本功能。这里的Event也是一种特殊的delegate。
使用event型的delegate可以在声明时加上关键词event
重点:event一旦被订阅(subscribed to),在摧毁前(destroy)一定要取消订阅(unsubscribeed from)。如果一直保持订阅,GC不会运行,导致内存泄漏(memory leaks)。实现它们的最好方式是在OnEnable()里订阅,在OnDisable()里取消订阅。
调用Events和调用delegate方式相同,这将调用订阅了该事件的所有函数。
event的重点在于触发类(triggering class)不需要告诉所有相关类(concerned classes)来调用特殊的方法。因为相关类可以监听一个event,然后做出相应的反应。
event和delegate的目的在于,比如我们现在将要创建的系统:我们已经有了场景管理器,现在我们需要卸载一个场景的同时,保存一些我们需要的数据。有两种方法可以实现它们,第一种,把场景控制器关联在所有需要保存的地方,然后再一 一“通知”这些地方保存。第二种,我们定义一个卸载场景的event,让这些需要保存的地方监听(订阅)该event,当event开始unload the scene时,保存。
lambda表达式是一种,将要被存在delegate里的方法的简写方式。它们的语法(syntax)如下
因为我们用lambda简写方法,那用来写lambda的变量也很简短。变量通常是隐式类型的(implicitly typed)
比如,x隐式的和someValue具有相同的类型。我们不知道x的类型,但知道someValue的类型,那么就能确定x的类型。
下面是一些用Lambda表达式写的delegate,都非常的简短
当Lambda表达式更长或多个表达式时,使用{},注意{}后面还有;。
使用Lambda expressions的目的是为了简化我们的代码
Scripts→MonoBehaviours→SceneCotrol文件夹下,打开SceneController脚本并编辑。
将会在Persistent Scene里新建一个空对象挂载该脚本。
接着Scripts→ScriptableObjects→DataPersistent文件夹,打开下面的SaveData并编辑(场景切换时保存一些必要的数据)
现在我们要把真正需要保存的数据(比如PositionSaver)和保存方式(SaveData)之间联系起来。Scripts→MonoBehaviours→DataPersistent文件夹下,打开Saver脚本。
接下来我们看看使用Saver的具体例子,同文件夹下打开PositionSaver脚本
比如读取Player的初始站位,则是SceneController
完毕。