Unity游戏系统之-RPG游戏剧情呈现策略

目录(?)[+]

喜欢我的博客请记住我的名字:秦元培,我的博客地址是:http://qinyuanpei.com 
转载请注明出处,本文作者:秦元培, 本文出处:http://blog.csdn.net/qinyuanpei/article/details/48435063

  各位朋友大家好,欢迎大家关注我的博客,我是秦元培,我的博客地址是http://qinyuanpei.com。今天博主想和大家探讨的是RPG游戏中剧情呈现的相关策略,我们知道一个RPG游戏主要是通过将玩家带入到游戏世界中进行某种“体验”进而影响角色的成长。这句话其实指出了RPG游戏的两个特点,即高度的代入感和角色的可成长性,因此我们在RPG游戏中能够看到以生命值、魔法值、经验值等为基础而展开的一系列的成长路线,玩家获得的每个经验值或者是技能点,从现实意义上来说都在表征角色的成长,而RPG游戏中宏大震撼的世界观和引人入胜的剧情则成为了玩家代入感的一种最为直接的表达方式。

一、RPG游戏==剧情?

  博主一直想强调的一个观点是游戏的本质是玩家和规则间的一种博弈,因此不管是什么样的游戏都要把握一个要素:游戏性。游戏性是什么?游戏性是一种策略,是一种玩法,是玩家对游戏规则的一种探索。RPG游戏虽然以剧情为核心,但是同样可以衍生出探索性,比如玩家在城镇中闲逛是对游戏世界观的一种探索,和NPC对话是对游戏剧情的一种探索,开宝箱收集物品是对游戏规则的一种探索等等,可是在《仙剑奇侠传六》中这一切都是不存在的,因为大家都认为只要有个好剧情就好了,这样可以赚足玩家的眼泪进而让玩家为游戏买单。可是《仙剑奇侠传六》的剧情真的是个好剧情吗?尽管从表面上看起来波澜壮阔、跌宕起伏,但是追究整个故事的起源你会发现都是编剧的“良苦用心”,鲲为了让禹族族人苏醒而“煞费苦心”,赢旭危为了让鲲“帮助”禹族而打开方便之门,扁洛桓为了让祈像个正常人一样生活而动“恻隐之心”。如果这些事情都没有发生,你觉得仙剑六的故事还会让人觉得这是一个好剧情吗?RPG游戏中的剧情是应该交给玩家去探索和挖掘的,然而现在国产RPG的编剧却喜欢用大量的篇幅去铺垫、埋伏笔甚至恨不得直接把剧情告诉玩家,整个游戏完全不像是玩家在探索这个陌生的世界,而是编剧每隔一段时间就会以上帝的身份来告诉你:亲,接下来XXX就要死了哦,请准备好纸巾和眼泪,我们会让你哭瞎眼睛的哦!甚至直接在游戏一开始的CG里就告诉玩家我们要讲一个什么样的故事,探索是一个RPG游戏的常态,在这个过程中迷宫、战斗、机关、对话在我看来都应该是对游戏剧情的一种补充,然而现在呢,现在变成了勉强当作幻灯片来看的一场皮影戏。

三位一体

  最近博主一直在玩Trine这个游戏,这个游戏的中文名称通常叫做三位一体或者魔幻三杰,和这个名称传达给我们的直观感受相同,这是一个讲述三位不同身份的英雄们一起踏上冒险之旅的游戏。虽然严格来说这个游戏并非RPG游戏,可是你在玩这个游戏的时候你无时无刻不在感受到剧情的推进,因为这三位英雄们(魔法师、女盗贼、士兵)不同的身份首先就会让你对整个剧情产生兴趣,可能是阴差阳错般地弄巧成拙让他们彼此相识,可能是冥冥之中自有天意安排他们彼此相见,他们每一个人背后有什么故事,为什么会被圣杯召唤并且聚集在一起?这就基本撑起了整个游戏的剧情了呀,所以它并不需要再占用大量的时间去铺垫和埋伏笔,因为剧情此时此刻就已经开始了。曾经玩《古剑奇谭2》的时候明显感到编剧在叙事方式上发生了变化,它那如上帝一般俯视人间的剧情视角,让玩家在探索游戏的过程中基本上看不到明确的立场,或许编剧希望玩家自己去寻找游戏中正确的立场,可是一个RPG游戏最基本的要求就是代入感啊,所谓代入感是先入为主、是感同身受而非冷眼观世,所以用小说的叙事视角来讲游戏剧情可能会让人记住一个好的故事但并不会让人记住一个好的游戏。我很欣赏北软团队想要追赶《古剑奇谭》系列的决心,他们确实为此付出了辛勤的劳动,可是偏偏不去学习人家优秀的东西反而退而求其次去学习人家这种不好的叙事方式、去学习人家不成熟的战斗系统、去学习人家被吐槽到面目全非的设定。距离7月8日仙剑六上市已经过去两个月了,仙剑六就像沉寂在冰下的水终于渐渐冷清下去,这段时间大家对游戏周边、MMD、同人视频、同人小说、Cosplay等的关注度要大大地超过对游戏本身的关注。我一直认为一个游戏是用来玩的,而非是用来看或者是追的,这样并非我们设计一个游戏的初衷不是吗?其实有时候我们自己的故事就像一个游戏,有时候我们被生活这个编剧无情地嘲弄着,有时候我们希望自己去扮演自己的角色。

二、过场动画还是即时演算?

  在游戏中呈现剧情基本上有过场动画和即时演算两种形式。过场动画通常是以视频的形式嵌入到游戏内容中,这种形式的优点是过场动画和游戏本身独立,因此过场动画的制作和游戏开发工作可以独立开来,从而节省研发过程中的时间提高开发效率。可是我们知道视频的画面表现取决于视频解码器的解码效果,因此为了保证视频的播放质量一般游戏中的过场动画文件体积都比较大,因此这种方式的缺点同样明显,即会增加游戏的容量。以最近的国产动作游戏《御天降魔传》为例,这部同样使用Unity3D引擎开发的单机游戏给《仙剑奇侠传六》神奇的优化问题树立了良好的榜样,游戏能针对玩家电脑的性能自动进行优化保证游戏可以在玩家电脑上流畅的运行。这个游戏的主线剧情长度较短,大概5到6个小时的时间就可以通关,可是这个游戏的容量居然达到了将近20G的程度,这是为什么呢?因为游戏里的所有过场动画都是以.avi格式的视频形式给出的,首先.avi格式是视频格式中比较重的一种格式,其次这个游戏里的所有视频是直接暴露给玩家的并没有对其进行压缩,我想这应该就是这个游戏容量居高不下的原因所在了吧!好了,其次是即时演算,即时演算本质上一组可执行序列依次执行的结果,类似我们熟悉的帧序列动画。即时演算是利用游戏引擎本身的强大特性去实现如角色移动和旋转、相机的移动和旋转、动画的播放、粒子效果、屏幕淡入淡出等剧情呈现的常见特性,因为即时演算的动画是实时播放的无需在本地预留视频文件,因此它可以有效地减少游戏容量,可是使用这种方式如果要做出真实的效果是不太容易的,因为它需要对每一个镜头进行细节上的把握,在游戏电影化的今天使用这种方式意味着需要把握更多的细节。为了写这篇文章,我在知乎上提了这样一个问题,大家有兴趣可以去看看啊!

三、在Unity3D中实现游戏剧情

  好了,那么我们可以关注这样一件事情了,在给玩家呈现剧情的时候,我们应该考虑到哪些因素呢?通常来讲,在给玩家呈现剧情的时候会涉及到摄像机及角色的旋转和移动、人物对话、背景音乐、动画、特效等等这些因素,如果我们把这个过程简化为一个线性的序列的话,我们会发现剧情呈现其实就是将剧情分解为一个线性的动作序列然后依次执行这个序列中的动作即可,这样如果实现了相机及角色的旋转和移动、人物对话、背景音乐、动画特效等每个动作的控制,那么将这些动作连贯起来就可以进行剧情的呈现了。好了,下面我们来看看具体是怎么样实现得吧!

 首先我们来定义一个基类RPGCommand

using UnityEngine;
using System.Collections;

public class RPGCommand
{
    public virtual void Execute()
    {

    }
}
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

  注意到Execute()方法是一个虚方法因此所有继承RPGCommand的类都必须要对这个方法进行重写,以显示对话框为例,我们可以由此派生出CommandDialog

using UnityEngine;
using System.Collections;

public class CommandDialog : RPGCommand
{
    /// <summary>
    /// 对话框头像
    /// </summary>
    private string header;

    /// <summary>
    /// 对话者姓名
    /// </summary>
    private string name;

    /// <summary>
    /// 对话框内容
    /// </summary>
    private string content;

    /// <summary>
    /// 对话框索引
    /// </summary>
    private int index;

    /// <summary>
    /// 构造函数
    /// </summary>
    /// <param name="header">对话框头像</param>
    /// <param name="name">对话者姓名</param>
    /// <param name="content">对话者内容</param>
    /// <param name="index">对话框索引,0表示左侧对话框,1表示右侧对话框</param>
    public CommandDialog(string header,string name,string content,int index)
    {
        this.header = header;
        this.name = name;
        this.content = content;
        this.index = index;
    }

    /// <summary>
    /// 构造函数
    /// </summary>
    /// <param name="dialog">对话信息</param>
    /// <param name="index">对话框索引</param>
    public CommandDialog(Dialog dialog,int index)
    {
        this.header = dialog.Header;
        this.name = dialog.Name;
        this.content = dialog.Content;
        this.index = index;
    }

    public override void Execute ()
    {
        Dialog dialog = new Dialog();
        dialog.Header = this.header;
        dialog.Name = this.name;
        dialog.Content = this.content;

        DialogSystem.Instance.SetDialog(dialog, this.index);
    }
}
   
   
  • 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
  • 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

 同理我们可以派生出其它的类,在设计每个派生类的时候我们应该着重关注Execute()方法的实现。好了,有了这些“演员”以后,我们需要有一个“导演”来对它们进行管理,因此接下来我们要定义一个CommandManager

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class CommandManager : MonoBehaviour {

    /// <summary>
    /// 命令列表
    /// </summary>
    private List<RPGCommand> commandList;

    /// <summary>
    /// 当前命令索引
    /// </summary>
    private int index=0;

    /// <summary>
    /// 当前命令类型
    /// </summary>
    public enum CommandType
    {
        Automatic,//自动执行命令
        Manually//手动执行命令
    }

    /// <summary>
    /// 当前命令类型,默认为自动
    /// </summary>
    private CommandType commandType=CommandType.Automatic;

    /// <summary>
    /// 命令管理器实例
    /// </summary>
    private static CommandManager instance;
    public static CommandManager Instance
    {
        get 
        {
            if(instance==null)
                instance=(CommandManager)GameObject.FindObjectOfType<CommandManager>();
            return instance;
        }
    }

    void Awake() 
    {
        //初始化命令列表
        commandList = new List<RPGCommand>();
        //初始化当前实例
        instance = this;
    }

    /// <summary>
    /// 向命令列表中添加命令
    /// </summary>
    /// <param name="command"></param>
    public void AddCommand(RPGCommand command)
    {
        if (commandList==null || command==null)
            return;
        commandList.Add (command);
    }

    /// <summary>
    /// 设置命令的类型
    /// </summary>
    /// <param name="type"></param>
    public void SetCommandType(CommandType type)
    {
        this.commandType = type;
    }

    /// <summary>
    /// 使索引递增的一个方法
    /// </summary>
    public void MoveNext()
    {
        //判断列表是否为空
        if (commandList == null || commandList.Count <= 0)
            return;

        index += 1;
        if(index >= commandList.Count) 
        {
            index=0;
            commandList.Clear();
        }
    }

    /// <summary>
    /// 自动执行下一句命令
    /// </summary>
    private void ExcuteNextAutomatic()
    {
        //判断列表是否合法
        if(commandList == null || commandList.Count <=0 )
            return;

        //判断索引是否合法
        if (index < 0 || index >= commandList.Count)
            return;

        //执行每一条命令
        if(index < commandList.Count)
        {
            commandList[index].Execute();
        }
    }

    /// <summary>
    /// 手动执行下一句命令
    /// </summary>
    private void ExcuteNextManually()
    {
        //判断列表是否合法
        if(commandList == null || commandList.Count <=0 )
            return;

        //判断索引是否合法
        if (index < 0 || index >= commandList.Count)
            return;

        //执行每一条命令
        if(index < commandList.Count) 
        {
            //获得第一条对话的索引
            int firstDialogIndex=GetFirstDialogIndex();


            if(index==firstDialogIndex)
            {
                //如果是是第一条对话则自动显示
                commandList[index].Execute();
                this.MoveNext();
            }
            else
            {
                //否则需要手动触发对话的显示
                if(Input.GetMouseButtonDown(0) || Input.GetKeyDown(KeyCode.Space))
                {
                    commandList[index].Execute();
                    this.MoveNext();
                    Debug.Log(commandList.Count);
                }
            }
        }
    }

    void Update()
    {
        switch (commandType)
        {
            case CommandType.Automatic:
                //自动执行下一句命令
                ExcuteNextAutomatic();
                break;
            case CommandType.Manually:
                //手动执行下一句命令
                ExcuteNextManually();
                break;
        }
    }

    private int GetFirstDialogIndex()
    {
        int result = -1;

        //遍历列表以获得第一个对话的索引
        for(int i = 0; i < commandList.Count; i++)
        {
            if(commandList[i].GetType() == typeof(CommandDialog))
            {
                result = i;
                break;
            }
        }

        return result;
    }
}
   
   
  • 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
  • 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

  因为通常来讲RPG游戏中的剧情对话是需要手动去触发的,因此在设计CommandManager的时候就有了Automatic(自动)和Manually(手动)两种形式,默认情况下剧情对话是手动的,具体的细节大家可以从代码中来研究,整个代码设计比较简单。

四、工程实例

 好了,接下来是一个测试用例:

using UnityEngine;
using System.Collections;

public class testMove : MonoBehaviour
{

    void Start ()
    {
        CommandManager.Instance.AddCommand (new CommandFadeIn(10));
        CommandManager.Instance.AddCommand (new CommandFadeOut(10));
        CommandManager.Instance.AddCommand(new CommandAudio("Audios/Event/e01"));
        CommandManager.Instance.AddCommand (new CommandMove ("Camera", 54.30874f,47.75602f,34.2717f,"",2.5f));
        CommandManager.Instance.AddCommand (new CommandRotate ("Camera", 0, 180, 0, "", 1.5f));
        CommandManager.Instance.AddCommand(new CommandDialogEnter());
        CommandManager.Instance.AddCommand(new CommandDialog("Textues/GUITxetures/Header/portrait00_02", "云天河", "大家好,我就云天河!", 0));
        CommandManager.Instance.AddCommand(new CommandDialog("Textues/GUITxetures/Header/portrait20_02", "慕容紫英", "云天河!立刻滚到思返谷思过!立刻!", 1));
        CommandManager.Instance.AddCommand(new CommandDialogExit());
    }
}
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

在这个用例中展示了画面的淡入淡出、相机的移动旋转、人物对话等内容,这样就实现了RPG游戏中的剧情呈现!下面是部分演示图片:

演示1

演示2

四、相关工具扩展

  我们注意到这里的剧情是用脚本来驱动的,因此大量的剧情逻辑是需要写在代码里的,这样对程序来说确实是友好的,可是如果让策划来做这件事情就不会那么友好了,如果策划具备一定的程序基础,那么这样尚可作为一个可以商措的方式;如果策划基本没有程序基础,那么抱歉我们需要换种方式来表达。考虑到Unity3D具备了较好的扩展性,因此我们可以考虑让这些命令文本化,这样就可以使用配置文件来处理这些命令,进而我们就可以使用程序来生成配置文件,换句话我们就有了工具,当然这工具是给策划使用的嘛!比如第三条命令我们可以使用这样的一行文本来表示:

audio,Audios/Event/e01;

第四条命令可以使用:

camera,54.30874f,47.75602f,34.2717f,,2.5f

来表示。当我们定义了这样一套规则以后事情就好办多了,遵守规矩的计算机程序会比不喜欢遵守规矩的人类更勤劳地去处理这些事情,这样可以减轻程序员的工作量,何乐而不为呢?再说一遍,程序员存在的意义在于提供合适的工具以更高效地完成工作,当策划和美术准备指挥程序做这里做那里去不去想这样做到底对不对的时候,我会说对不起我没有这样的义务。好了,今天的内容就是这样了,我知道有很多人都希望我写点普通人“抄点代码”就能用的教程,而不是这样看起来高大上实则没有什么卵用的东西,可是我觉得做人不能这样浮躁,虽然站在巨人的肩膀上没有什么错,可是如果你什么东西都等着别人给你提供现成的,你终究不会学到什么真正的东西,因为学习是从外部逐渐内化的过程,经常有人问我学习Unity3D有没有什么样的教程,我就搬出基本没有再更新的Unity圣典和最近的Unity3D官方文档给他看,英文不好的可以去看中文翻译版,英文稍微好点的可以去看官方的英文版本,在我看来这就是最好的教程了。我知道我说看文档这样的话有许多人不高兴,但是看文档确实可以解决很多人的问题。好了,今天同样是提供一个思路,大家可以自行深入研究,希望大家喜欢!

喜欢我的博客请记住我的名字:秦元培,我的博客地址是:http://qinyuanpei.com 
转载请注明出处,本文作者:秦元培, 本文出处:http://blog.csdn.net/qinyuanpei/article/details/48435063

  • 8
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值