简介

该项目最初只是因为元夜描图画了一个元夜子的icon,在交流的过程中,一个游戏项目便悄然诞生。

游戏类型:2D横版酷跑游戏

人员

程序:抹茶

美术:元夜&抹茶

策划:元夜

引擎:Unity3D Pro

IDE:Visual Studio for Mac

新技术记录

单例模式的相关基类

泛型基类(无关Mono)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
using System;

/// <summary>
/// 单例模式的泛型基类,不针对U3D对象。
/// </summary>
/// <typeparam name="T"></typeparam>
public class SingletonBase<T> where T : class//new(),new不支持非公共的无参构造函数
{
private static T m_instance;
private static readonly object SyncObject = new object();

/// <summary>
/// <T>类型的单例
/// </summary>
public static T Instance
{
get
{
// 没有第一重 singleton == null 的话,每一次有线程进入 GetInstance()时,均会执行锁定操作来实现线程同步
// 非常耗费性能 增加第一重singleton ==null 成立时的情况下执行一次锁定以实现线程同步
if (null == m_instance)
{
lock (SyncObject)
{
if (null == m_instance) //Double-Check Locking 双重检查锁定
{
//m_instance = new T(); // 需要非公共的无参构造函数,不能使用new T() ,new不支持非公共的无参构造函数
m_instance = (T)Activator.CreateInstance(typeof(T), true); //第二个参数防止异常:“没有为该对象定义无参数的构造函数。”
}

}
}
return m_instance;
}
}
}

会自动销毁的Mono单例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
using UnityEngine;

/// <summary>
/// 继承了Mono的单例模式的基类,会进行销毁
/// </summary>
/// <typeparam name="T">类的名称</typeparam>
public class SingletonMono<T> : MonoBehaviour where T: MonoBehaviour
{
private static T m_instance = null;

//防止部分不销毁物体重复在场景重载后重复

public static T Instance
{
get { return m_instance; }
}

protected virtual void Awake()
{
m_instance = this as T;
}
}

游戏运行过程中保证唯一且不会销毁的Mono单例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
using UnityEngine;

/// <summary>
/// 继承了Mono的单例模式的基类,会保证全局唯一性
/// </summary>
/// <typeparam name="T"></typeparam>
public class SingletonAutoMono<T> : MonoBehaviour where T : MonoBehaviour
{
private static T m_instance = null;

/*
GameManager 是在第一个场景里创建的
当我们从第二个游戏场景切换回第一个游戏场景的时候
Unity 并不是恢复第一个游戏场景,而是会重新创建出一个新游戏场景
这就会导致一个新的 GameManager 对象被创建。
为了保证 GameManager 对象的唯一性
需要在 GameManager 类的 Awake() 方法里增加一些逻辑判断
当检查到已经有一个 GameManager 对象存在的时候
就把当前的 GameManager 对象销毁

*/
//防止部分不销毁物体重复在场景重载后重复

public static T Instance
{
get
{
if (m_instance == null)
{
GameObject obj = new GameObject();
//设置对象的名字为脚本名
obj.name = typeof(T).ToString();
//让这个单例模式对象 过场景 不移除
//因为 单例模式对象 往往 是存在整个程序生命周期中的
DontDestroyOnLoad(obj);
m_instance = obj.AddComponent<T>();
}
return m_instance;
}
}

}

参考

继承自Monobehavior基类的子类Start函数的调用

在使用继承自Monovehavior的父类时,其子类如果实现Start等Unity方法时,只会优先实现子类的Start方法,其父类中的代码段不会被执行。

而在实现Animal基类时,期待可以在基类的Start方法中初始化碰撞体。

为了解决这一问题,我们可以在Animal基类中实现类似于Unity的Start等方法,在基类的Start等方法中调用单独实现的对应虚方法,如果子类需要调用则重写基类相关方法,否则可以不写。

设置为虚函数可以保证子类并非强制重写,而如果设置为抽象函数,那么无论子类是否需要进行实现,都需要对此进行重写,因此这里采用虚函数的方式。

其基类对应的实现如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;

public abstract class Animal : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
OnStart();
}

void Awake()
{
OnAwake();
}

// Update is called once per frame
void Update()
{
OnUpdate();
}

/// <summary>
/// 因为需要继承,子类调用的Start方法
/// </summary>
protected virtual void OnStart()
{

}

/// <summary>
/// 因为需要继承,子类调用的Update方法
/// </summary>
protected virtual void OnUpdate()
{

}

/// <summary>
/// 因为需要继承,子类调用的Awake方法
/// </summary>
protected virtual void OnAwake()
{

}
}

对应子类的实现方式如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Child : Animal
{
// Start is called before the first frame update
protected override void OnStart()
{

}

// Update is called once per frame
protected override void OnUpdate()
{

}
}

参考链接:关于继承MonoBehaviour的一些记录

观察者模式与事件中心

在玩家触碰到怪与死亡的时候,需要UI界面减小体力,怪物接收到指令后执行相应的事件。

为此,我们有三种方式

  • 拉取
    • 方式:在Player中设置一个状态变量,比如isAlive,在死亡的时候改变该值,其他需要获取的脚本在每帧检测,在对应时机执行。
    • 缺点:不及时,拉取实际只能为下一帧,该方式比较消耗性能
  • 推送
    • 方式:在Player中保存所有需要监听的组件,当死亡时向Player维护的数组中所有的组件发送死亡消息(调用对应的方法)
    • 缺点:拓展性差,紧耦合,具体表现为,无论实现什么新的方法都需要Player去进行关心,如果在联合开发的时候,每次开发新的功能都需要其他人来告诉Player部分的开发者添加什么样子的功能。
  • 事件:
    • 方式:使用委托构建事件系统,在需要调用的实际调用该委托类的Invoke方法,其他需要监听的地方调用并且添加相应的监听方式(AddEventListener)
    • 优点:结合前两种方式的优点

其对应代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
using System.Collections.Generic;
using UnityEngine.Events;


public interface IEventInfo{ }
public class EventInfo : IEventInfo
{
public UnityAction Action;
}
public class EventInfo<T> : IEventInfo
{
public UnityAction<T> Action;
}


public class EventSystem : Singleton<EventSystem>
{

public Dictionary<string, IEventInfo> actionDictionary = new();

// 添加事件 - 没有泛型
public void AddEventListener(string name, UnityAction action)
{
if (actionDictionary.ContainsKey(name)) {
(actionDictionary[name] as EventInfo).Action += action;
}
else
{
actionDictionary.Add(name, new EventInfo() { Action = action});
}
}

// 添加事件 - 有泛型
public void AddEventListener<T>(string name, UnityAction<T> action)
{
if (actionDictionary.ContainsKey(name))
{
(actionDictionary[name] as EventInfo<T>).Action += action;
}
else
{
actionDictionary.Add(name, new EventInfo<T>() { Action = action });
}
}

// 移除事件 - 没有泛型
public void RemoveEventListener(string name, UnityAction action)
{
if (actionDictionary.ContainsKey(name))
{
(actionDictionary[name] as EventInfo).Action -= action;
}
}

// 移除事件 - 有泛型
public void RemoveEventListener<T>(string name, UnityAction<T> action)
{
if (actionDictionary.ContainsKey(name))
{
(actionDictionary[name] as EventInfo<T>).Action -= action;
}
}

// 调用事件 - 没有泛型
public void TriggerEventListener(string name)
{
if (actionDictionary.ContainsKey(name))
{
(actionDictionary[name] as EventInfo).Action?.Invoke();
}
}

// 调用事件 - 有泛型
public void TriggerEventListene<T>(string name, T paramater)
{
if (actionDictionary.ContainsKey(name))
{
(actionDictionary[name] as EventInfo<T>).Action?.Invoke(paramater);
}
}

// 清空事件
public void Clear()
{
actionDictionary.Clear();
}
}

// https://www.bilibili.com/video/BV12h411H7gB

开发日志

2022-02-05: 正式建立项目

2022-02-20: 完成开发UI框架

2022-03-05: 第一次与元夜子进行面对面沟通意见

2022-03-06: 完成故事读取(含联网获取)框架

2022-03-12: 第二次与元夜子进行面对面交换意见

2022-03-20: 编写Animal类及其子类,制作Animals预构件

2022-03-25: 完善Player和Enemies的预构件

2022-04-03: 将加载界面和文字滚动显示界面嵌入UIFramework

2022-04-19: 添加背景移动(及基类等)和角色移动控制。

2022-06-18: 重构背景层素材,并完善背景的分层移动和昼夜切换