Unreal基于对象的语音系统

Dialogue System In Unreal

Posted by 李AA on August 21, 2019

前言

  • Dialogue Voice System是Unreal的音频系统中为数不多的亮点。这个工具在实际使用流程中比较流畅,组合性也很强大,虽然有一定维护成本,但是值得借鉴分析。

  • 设计思路

    1. Single Listener

  • 演示视频
  1. Multi Listeners

  • 演示视频
  • 可组合效果

    1. 单个玩家和单个NPC每次对话的不同
    2. 单个玩家和多个NPC每次对话的不同
    3. 单个玩家不同状态和同一个NPC对话不同
    4. 多个玩家和同一NPC对话不同
    5. 多个玩家不同状态和同一NPC对话不同
  • 使用步骤

  1. listeneremitter创建Dialogue Voice(对话个体)组件,在组件中设置标签

  2. 为一个对话场景创建一个Dialogue Wave组件(对话逻辑组),组件中设置Dialogue Voice之间的关系以及具体播放声音对象

  3. 可以创建一个Dialogue Wave和具体对话场景关联的容器类来管理每个Dialogue Wave(对话逻辑组)和具体场景(scene)的关系

  4. 最后设置播放逻辑

系统

Dialogue Voice

  • Dialogue Voice相当于对话对象组件,用来标识相同的一类对话对象,便于在Dialogue Wave中进行逻辑设置

  • 语音系统的Voice组件默认提供了两个标签GenderPlurality。标签应该可以扩展,以便更加精细的对对话对象进行分类

  • 同一个人物对象可以有多个对话对象组件,用来表示不同状态,不同时刻人物对话内容的差异。

Dialogue Wave

  • Dialogue Wave相当于对话逻辑组。用来组合多个Dialogue Voice和具体的声音素材,同时可以进行本地化语言的设置

  • 对话逻辑组设计之前应该对所有对话素材按scene进行整理分类,对于每个scene尽量用一个对话逻辑组来表示。同一个人物对象可以关联多个不同的对话逻辑组,对应不同的对话对象和对话场景。

接口

  • 下面用代码快速原型一下这个系统,以下是我自己对系统的理解,不是Unreal源代码
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
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
#include <string>
#include <vector>
#include <map>

//------------------------------DialogueVoice用到的Tag------------------------------------------------------
enum Gender
{
    Neuter,
    Masculine,
    Feminine,
    Mixed
};

enum Plurality
{
    Singular,
    Plural
};

//----------------------------简略SoundCue类,可以播放音频文件------------------------------------------------
class SoundCue
{
public:
    void Play() {}

private:
    const std::string m_SoundPath;
};

//------------------------------Dialogue Voice类,可以设置标签------------------------------------------------
class DialogueVoice
{
public:
    DialogueVoice() : m_Gender(Neuter), m_Plurality(Singular) {}

    ~DialogueVoice() {}

    void SetGender(const Gender type)
    {
        m_Gender = type;
        return;
    }

    const Gender &GetGender() const
    {
        return m_Gender;
    }

    void SetPlurality(const Plurality plurality)
    {
        m_Plurality = plurality;
        return;
    }

    const Plurality &GetPlurality() const
    {
        return m_Plurality;
    }

private:
    Gender m_Gender;
    Plurality m_Plurality;
};

//-----------------------------------Dialogue Wave类的基本数据结构-------------------------------------
struct DialogueContexts
{
    DialogueContexts() : listeners(1) {}

    DialogueVoice *speaker = nullptr;

    std::vector<DialogueVoice *> listeners;

    SoundCue *sound = nullptr;
};

//-----------------------------------Dialogue Wave类,维护对话逻辑组-------------------------------------
class DialogueWave
{
public:
    DialogueWave() : m_Contexts(1) {}

    ~DialogueWave()
    {
        if (m_Contexts.size() > 0)
        {
            for (auto context : m_Contexts)
            {
                if (context->listeners.size() > 0)
                {
                    for (auto listener : context->listeners)
                    {
                        delete listener;
                        listener = nullptr;
                    }
                }
                delete context;
                context = nullptr;
            }
        }
    }

    int AddDialogueContext()
    {
        DialogueContexts *newContext = new DialogueContexts();
        m_Contexts.push_back(newContext);
        return m_Contexts.size() - 1;
    }

    void DeleteDialogueContext(int contextIndex)
    {
        if (m_Contexts.size() >= 1 && contextIndex < m_Contexts.size())
        {
            m_Contexts.erase(m_Contexts.begin() + contextIndex - 1);
            return;
        }
        else
            return;
    }

    void SetSpeaker(int contextIndex, DialogueVoice *speaker)
    {
        if (contextIndex < m_Contexts.size())
        {
            if (speaker)
                m_Contexts[contextIndex]->speaker = speaker;
            else
                return;
        }
        else
            return;
    }

    int AddListener(int contextIndex)
    {
        if (contextIndex < m_Contexts.size())
        {
            DialogueVoice *newListener = new DialogueVoice();
            m_Contexts[contextIndex]->listeners.push_back(newListener);
            return m_Contexts[contextIndex]->listeners.size() - 1;
        }
        else
            return -1;
    }

    void SetListeners(int contextIndex, int voiceIndex, DialogueVoice *dialogueVoice)
    {
        if (contextIndex < m_Contexts.size())
        {
            if (voiceIndex < m_Contexts[contextIndex]->listeners.size() && dialogueVoice)
            {
                m_Contexts[contextIndex]->listeners[voiceIndex] = dialogueVoice;
                return;
            }
            else
                return;
        }
        else
            return;
    }

    void DeleteListener(int contextIndex)
    {
        if (contextIndex < m_Contexts.size())
        {
            if (!m_Contexts[contextIndex]->listeners.empty())
            {
                m_Contexts[contextIndex]->listeners.pop_back();
                return;
            }
        }
        else
            return;
    }

    void SetSound(int contextIndex, SoundCue *soundCue)
    {
        if (contextIndex < m_Contexts.size())
        {
            if (soundCue)
            {
                m_Contexts[contextIndex]->sound = soundCue;
                return;
            }
            else
                return;
        }
        else
            return;
    }

    void ClearSound(int contextIndex)
    {
        if (contextIndex < m_Contexts.size())
        {
            if (m_Contexts[contextIndex]->sound)
            {
                m_Contexts[contextIndex]->sound = nullptr;
            }
            else
                return;
        }
        else
            return;
    }

    const std::vector<DialogueContexts *> &GetContexts() const
    {
        return m_Contexts;
    }

private:
    std::vector<DialogueContexts *> m_Contexts;
};

//-----------------------------可选类,管理DialogueWave和具体场景scene关联-----------------------------------------
class DialogueManager
{
public:
    int AddDialogueScene(const std::string &sceneName, DialogueWave *dialogueWave)
    {
        if (!sceneName.empty() && dialogueWave)
        {
            std::pair<const std::string &, DialogueWave *> newPair(sceneName, dialogueWave);
            DialogueSceneMap.insert(newPair);
        }
        else
            return -1;
    }

private:
    std::map<const std::string &, DialogueWave *> DialogueSceneMap;
};

//-------------------------------游戏角色类,可以拥有多个Dialogue Voice---------------------------------------------
class Actor
{
public:
    const std::vector<DialogueVoice *> &GetAllDialogueVoiceComponent() const
    {
        return m_DialogueVoice;
    }

private:
    std::vector<DialogueVoice *> m_DialogueVoice;
};

//------------------------最终的全局play函数,在提供的Dialogue Wave中寻找是否有涉及Actor的Dialogue Voice--------------
//  如果有的话,播放对应的SoundCue
void PlayDialogueAtLocation(Actor *actor, const DialogueWave *dialogueWave)
{
    if (actor && dialogueWave)
    {
        auto dialogueVoices = actor->GetAllDialogueVoiceComponent();
        for (auto dialogueVoice : dialogueVoices)
        {
            auto contexts = dialogueWave->GetContexts();
            for (auto context : contexts)
            {
                for (auto listener : context->listeners)
                {
                    if (listener == dialogueVoice)
                    {
                        context->sound->Play();
                    }
                }
            }
        }
    }
    else
        return;
}

结语

  • 快速原型了一遍dialogue系统后更加觉得Unreal这个设计的简洁实用,无论对于开发端还是用户端都比较友好,也有良好的扩展性,可以借鉴思路移植到其他游戏开发平台。