1、观察者模式:
适合多个无关的系统通信时使用,它使得代码能够发出一个消息,并通知对消息感兴趣的对象,而不用关心具体是谁接收到了通知。
观察者系统和事件系统的区别:观察者系统观察某个系统对象里发生的一些事,事件系统观察系统里的某个事件对象是否发生
2、使用工具:
Unity2022.3.10f1c1、visualstudio2019
3、实现内容:
抱着要找两个无关系统观察另一个系统的想法,选择了做背包系统、技能系统观察到物理系统里玩家拾取物品后,更新背包里的物品并解锁技能的功能(并没有完整实现,只是想象了一个应用场景)
4、代码及思路:
本来想照着书上写的那个逻辑做的,但是比较奇怪的一点是书上的被观察者知道观察者的存在,观察者却不知道被观察者的存在(即subject类里有observer类的指针,而observer类里没有subject),并且想着要结合C#的event关键字来做,所以逻辑改为了观察者observer知道被观察者subject的存在,被观察者subject不知道观察者observer的存在。
在C#里要实现这个非常简单,用委托就行了,我这里选择了Action,其实Delegate、Event、Func都行,如果需要在Inspector里添加方法的话用UnityEvent更好。可参考我的这篇C#中的delegate、event、action及匿名函数(lambda表达式)_c# event action-CSDN博客
写了三个脚本,一个subject类,两个Observer类,除此以外还有一个控制玩家移动的Player类,功能是玩家去触碰到场景里的小球就会发出消息告知BagObserver类和SkillObserve类根据类型捡起(销毁)它,并在控制台输出
Player类
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[RequireComponent(typeof(Rigidbody))]
[RequireComponent(typeof(Collider))]
public class Player : MonoBehaviour
{
public float moveSpeed=1.0f;
private void OnValidate()//强制设定值保证player能进行碰撞检测
{
GetComponent<Rigidbody>().isKinematic = true;
GetComponent<Collider>().isTrigger = true;
}
void Update()
{
// 获取输入的水平和垂直轴值
float moveHorizontal = Input.GetAxis("Horizontal");
float moveVertical = Input.GetAxis("Vertical");
// 计算移动方向
Vector3 movement = new(moveHorizontal, 0.0f, moveVertical);
// 移动玩家
transform.Translate(movement * moveSpeed * Time.deltaTime, Space.World);
}
}
这个脚本已经把一些需要的碰撞体组件以及属性设置好了
Subject类
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
public class Subject : MonoBehaviour
{
public Action<GameObject> Pickup;
private void OnTriggerEnter(Collider other)
{
//Debug.Log(other.name);
Pickup?.Invoke(other.gameObject);//检查pickup是否为空再调用
}
}
我使用Action委托来将观察者方法变量化,Subject脚本挂在Player物体上,Player触碰到其他碰撞体时就会发出消息(执行Pickup上的所有方法引用),但是并不关心谁收到了消息
BagObserver类
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class BagObserver : MonoBehaviour
{
public Subject subject;
private void Start()
{
subject.Pickup += PickUpProp;//注意不使用“=”来赋值,因为会覆盖原来的方法
}
public void PickUpProp(GameObject prop)
{
if (prop.CompareTag("Prop"))
{
//和ui连起来
Debug.Log("Pick up prop");
Destroy(prop);
}
}
}
SkillObserver类
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SkillObserver : MonoBehaviour
{
public Subject subject;
private void Start()
{
subject.Pickup += PickUpSkill;
}
public void PickUpSkill(GameObject skill)
{
if (skill.CompareTag("Skill"))
{
//和ui连起来
Debug.Log("Pick up skill");
Destroy(skill);
}
}
}
这两个Observer类长得基本上一模一样,因为我省去了和ui的连接。声明一个subject变量来获得被观察者subject的引用,如果需要的话还可以在脚本里创建很多个被观察者subject,同时观察多个被观察者subject,实现多对多的关系。通过比较拾取物品的标签的方式来判断是否要执行后面的方法。
5、Unity里的设置
这一块很简单,对unity比较熟的、看懂代码的同学应该可以直接按自己的想法在场景里设置好,可以不看下面的步骤
1)创建地面(plane)、Player(capsule)、Props(empty)、Skills(empty)、Manager(empty),再在Props和Skills里分别创一些小球(sphere)作为要被拾取的物体
给这些小球创建一些不同的材质进行区分(不设置也行)
2)将Subject、Player脚本挂在Player物体上
将两个Observer脚本挂在Manager上
将Player上的Subject拖给它们
3)给每个小球设置tag
先添加上Prop和Skill两个tag
一一对应的赋给Props和Skills下面的子物体,每一个都需要哦。
6、运行效果展示
不知道为什么有一点发黄...
可以看到Player触碰到小球后就会触发不同的观察者的方法
7、感想
总觉得这个观察者模式在C#里实在是过于简单了,不知道是不是我有哪里误会了吗?但是确实达到了观察者模式的功能,或许这就是C#被选作unity的脚本语言的原因吧。如果有问题的话希望评论区大佬指正