2024GGJ项目复盘——吸血鬼LIKE与半Roguelike

本片文章系个人总结,无任何教学指导意义

前言

        接下来的这几天将更多地围绕这次GGJ的项目进行复盘,大概思路就是先分析项目构成,然后分解正几个部分,回忆制作问题,最后提出改进方案。但是最后这点可能不会那么顺风顺水。

        我们项目主要成员只有四个:两个程序,一个美术,一个策划。在还没正式开始项目前,我们自信满满地一致同意想做一款《吸血鬼LIKE》游戏,或者更多的是模仿了《土豆兄弟》,反正无论如何整个游戏类型大体分支应该是RPG>Roguelike/半Roguelike。

        这次我们几个做了整整7天有余,什么?GGJ哪有7天?我们当然是线上抢跑的。Make Me Laugh 这个主体已经不重要了,它带给我们的启示仅仅只是让我们放宽心去做一个“比较搞笑”的游戏,于是乎,这款烂梗跟BUG并存的轻度Roguelike游戏诞生了——Emoji  大神。

http://【【2024GGJ】Emoji大神:互联网抽象一日游】https://www.bilibili.com/video/BV1UZ4y1E7wQ?vd_source=1f82fa35bcae900f43192b90412af5f7

 

         B站视频链接,不是广告。

        这篇文章也只是对整个项目进行记录复盘个大概,因此不会对代码内容进行解释。

游戏构成

介绍:

        我们的游戏以一个滑稽Emoji作为主角,在“贴吧”领域生存,在一波又一波“怪物”的攻击下,存活并升级,更新装备,最终活到20波后游戏胜利。

        整个游戏是2D平面俯视角。具体画面设计可以看看链接的视频。

        这里的游戏构成将是我从一个程序的角度出发,因为我们选择了半逆半正的工作模式,啥意思呢,就是正向有我们自己构思的结构,逆向有我们借鉴其他游戏分析得出的结构(抄袭)。

其实这个半逆半正并非我所设想,只是在我和策划出现思路分歧时不得已采取的“求同存异”方案。

        这个问题经过我们的谈论,原因在程策沟通少,加上我们团队组织上的存在的原因。程序(我)很喜欢按自己的构想来写,有时候是我自作主张,有时候是策划案里没有明确设计,所以我当机立断,占山为王(自己想怎么样就怎么写)。策划是因为没有和程序讲清楚,导致思路对不上(这不是我自己说的,是策划说的)。还有一个根本因素就是,其实我们团队没有明确岗位,大家都是会啥做啥,所谓的什么程序策划单纯是方便在报名时填那个表,所以我们的策划不像岗位上的策划一样要策划很多东西,更多的是记录一下开发日志,更新开发进度,整理开发数据。

        我们其他什么程序美术也会有自己想加进游戏的东西,那个时候我们就也变成了策划,所以整个游戏可以说是大家策划的,策划负责管理的。正因如此,我们才不免的出现与策划原本设计不一样的问题,有时是因为策划没想得那么细,大家都在凭感觉做游戏。

主要部分:

1.战斗系统

2.商店系统

3.表格系统

两个主要部分,也对应了两个主要场景。

战斗系统

1.玩家

        玩家Player作为玩家得主要行动单位,负责接受玩家输入和战斗,没什么好说的。

但是我却在一开始就埋了祸根,一是我并没有把玩家跟敌人看作是同一种东西,或者说他们没有继承自同一个父类,因为我一开始并不觉得玩家和敌人有很多共性。结果一看策划案!完蛋了。

        具体情况就是,我觉得玩家有武器,暴击率,伤害加成等等,然后敌人没有,但后来发现策划案里敌人和玩家同源,玩家有敌人没有的属性可以设为0就好了。这是第一个小坑。

        第二个就是玩家类,或者说单位类代码上的问题,就是我太执着于属性了(Property),每个属性我都想设计得很合适,注意我说的这个属性应该是(Attributes),Property属性是一种相对安全的设计方案?但是属性(Attributes)是我们要投入游戏里的数据。也就是说我太执着角色的每个Attribute都用Property来封装,代码看起来就比较冗杂。当然我不是说属性不好,我只是说单纯用字段也挺方便的。可能遇到功能复杂的系统会有很多属性需求,但是我在做的时候感觉用字段非常方便,可能还没到需要很多属性的境界吧。

        角色的移动方面我用了新输入系统,该说不说非常好用。

private void Move()
{
    Vector2 vector2;
    vector2 = BaseControls.Player.Movement.ReadValue<Vector2>();    
    rb.velocity = vector2 * MoveSpeed;    
}

三言两语就解决了基本移动,这里的BaseControls是自定义的Input Actions,不过要记得生成代码 。大道至简 ✔

2.敌人

        如同上面所说的,我因为一开始的设计问题,把两个可以一样的东西设计成了一样但是多写一份代码的东西。所以说制作之前的设计和构思非常重要,要对想做的东西经过一定思考,脑补框架和结构。事实上,我在后面做商店系统的时候就成功收益了。

        敌人的移动和玩家类似:

protected virtual void BaseAction()
{
    //BaseAction: Chasing Player
    Vector2 playerPos = Player.Instance.transform.position;
    Vector2 ownPos = transform.position;
    Vector2 dir = new Vector2(playerPos.x-ownPos.x,playerPos.y-ownPos.y).normalized;
    rb.velocity = dir * MoveSpeed;
}

        这里是持续追击玩家,当然有留个virtual等着重写,因为还设计有远程敌人不会追击玩家但会射击。  

        这里的追击十分简单,没有复杂的算法。大道至简×2 ✔

3.武器

        设计上武器分为两种,近战武器和远程武器。近战武器会绕着玩家转圈,远程武器会自动攻击最近的敌人。

        近战武器的机制还好说一句话的事:

private void FixedUpdate()
{        
    transform.RotateAround(Player.Instance.transform.position, Vector3.forward, Omiga * Time.fixedDeltaTime);                
}

这里因为是2D俯视角所以旋转轴跟Z轴平行就好了。

        远程武器实现方式大概就是通过触发器Trigger检测范围内敌人,更新敌人列表,排序敌人位置,选择距离最近的敌人射出子弹。

战斗系统总结

        这个战斗系统不算复杂,但是还是有一些小问题,比如远程武器的判别方式可能会导致一些空引用的错误,因为不影响运行我当时就没改。

        其实我个人觉得这样射击战斗方式还挺有意思的,因为我个人很喜欢模拟器类型,RTS类型的游戏,自动战斗是非常重要的,又简单有有效,让程序设计可以往更多方向深入。当然游戏类型不同侧重点也不一样,像格斗游戏那种对打击感有很大需求,所以战斗机制非常复杂。

        除此之外战斗系统应该还包括属性的升级等方面,这里就完全按策划案里的去做。但是我觉得我写的还不是很好,算是一个待改进方面。 

商店系统

        这个商店系统一开始并不是由我来做的,但是后来合并的时候发现功能与需求不一样,又得我来补救,所以策划一定要与各个岗位沟通好才行。

1.商店管理器

        这个东西是整个商店系统的核心,很多相关功能和商店UI的处理就是写在这个脚本。 

特别地,这里对UI的处理我还是有所领悟的,因为一开始做的时候本来想尝试一下兼容手柄的,

但是实际上手后发现很多问题:

        1.手柄操作导航问题:

        在整个商店UI上有很多按钮,比如说那些一个个的格子,刷新,下一波按钮。这些按钮的导航如果不加以设置的话会出现很多问题:

        试想一下,当你想出售一个东西时,会弹出确认出售的按钮,我自然而然的会用一个Panel挡住确认面板后面的UI元素,避免鼠标点击到它们。但是如果你用手柄操作,那么会面临两个问题,一是面板弹出后,不会自动跳转到子面板上的按钮,所选择的对象还是原来的那个;二是虽然鼠标无法接触到选择对象(因为被Panel挡住了),但是导航却可以导航到Panel后面的面板。如果误触将会导致很多BUG。

        第一个问题还好解决,只要代码调整一下当前事件系统的选择对象即可。

        但是第二个问题我曾尝试过写一大堆判断方法规范按钮之间的导航,但是结果又是一堆BUG,真不如直接手动设置导航。比如按钮,就在组件的Navigation里选择Explicit就好了,然后分别指定或留空四个方位的导航。

        2.鼠标互动的问题

        我说过,一开始我是想讲这个游戏制作成兼容手柄的,或者说,本身是为手柄诞生的。于是我在设计一些UI交互的时候,不得不与Selectable打交道。比如,当选择对象改变时,改变提示信息,就像下面要讲的商店提示。

        但是鼠标操作有一个问题就是只有在点击触发后才会改变选择对象,所以想要更改信息就得一直点击,非常不方便。所以就得用代码检测鼠标停留处的UI元素,并将其变成当前选择的对象。

可能看得不太明白,总的来说我所追求的操作手感趋同于怪猎的那种。

2.商店格子

        要提到这个,就必须先简单提一提表格数据输入对整个游戏的影响了,有了表格数据的加持,可以让我摆脱狂造预制体的悲惨命运。

        这个商店格子其实就是一个很简单的脚本。

using System;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
[RequireComponent(typeof(Button))]
public class Block : MonoBehaviour
{
    public Image image;
    public Image color;
    public TextMeshProUGUI levelText;
    [Header("储存的信息")]
    public BlockInfo blockInfo;
    [Header("等级范围对应的颜色")]
    public RankColor[] rankColors;

    private void Update()
    {
        RefreshImage();
    }
    //根据格子自己的信息刷新图像显示
    public void RefreshImage()
    {       
        //略...
    }
    //根据等级设置底板颜色
    private Color SetColor(int rank)
    {
        //略...
    }

    [Serializable]
    public class RankColor
    {
        [Header("对应的等级范围")]
        public int MinRank;
        public int MaxRank;
        [Header("对应的颜色")]
        public Color color;
    }
    
}

重点在于一个叫BlockInfo的自定义结构体 :

[Serializable]//unity inspector监视可序列化类型,对类可用
public struct BlockInfo
{
    public BlockType type;
    public int index;

    public BlockInfo(BlockType type = BlockType.Null, int index = 0)
    {
        this.type = type;
        this.index = index;
    }
}
public enum BlockType
{
    Null,
    Weapons,
    Props,    
}

顾名思义,BlockInfo结构体用来记录当前这个格子的信息,只有两个:格子类型和编号。

在此前,我已经将表格数据存入好几个字典,并通过编号访问对应信息。

比如这里有武器字典,道具字典。

所以这几个格子只需要存储对应物品类型和编号,我就可以通过它们找到该物品的信息,然后将其读取并对提示信息作出修改。

3.商店提示

        这个商店提示和格子大同小异,思路就是,判断当前选择对象是不是一个格子,如果是那么就获取Block组件,再获取BlockInfo,最后根据类型,编号,查字典找到对应信息,最好刷新一下即可。

商店系统总结

        虽然是赶工赶出来的,但是得益于开工前思考,做出来的效果还是不错的,也总结了很多小细节,小问题。                       

        也许看不懂,但是整个商店系统,甚至整个游戏的数据其实都是建立在读表的基础上实现交互地。所以重点其实是读表嘿嘿。

表格系统

游戏里的导入的外部表格格式都为CSV。

过去式

        其实这根本不算个系统,我只是为了突出在做这个项目时读取表格对我的影响和意义。

其实我也是后期才发现读表的强大和重要性。一开始因为赶工,我直接抱着有多少种怪物就做多少个预制体的打算,但是策划给我发了个表是这样的:

好吧,这其实我是经过考虑后设计的表格并让策划把数值填上去的。但是主要问题是,有相当一部分怪物是用的同一个预制体,比方说小丑。但是我不可能根据这个表去制作22个相同的预制体,不同的只有预制体身上挂载的属性的数据吧(小丑是个预制体,挂载有敌人组件)。

        况且预制体是静态的,所以我们只能在生成实例后对实例上的敌人组件赋值,思路大体是:

根据编号生成预制体,根据编号对应的每行的信息给预制体上的组件赋值。

        这也是为什么我要在每一行之前设置一个编号。

private void InitializeEnemiesList()
{        
    string path = Path.Combine(Application.streamingAssetsPath, "ExtendData/怪物编号表.csv");    
    using (FileStream fs = new(path, FileMode.Open, FileAccess.Read))
    {
        using (StreamReader sr = new StreamReader(fs, Encoding.UTF8))
        {
            string line = "";
            bool firstLine = true;  //省略首行
            while ((line = sr.ReadLine()) != null)
            {
                if (firstLine)
                {
                    firstLine = false;
                    continue;
                }
                string[] s = line.Split(",", 2);
                EnemiesList.Add(int.Parse(s[0]), s[1]);
            }
        }
    }

}

这是其中一个数据处理的方法,对象就是上面的那个表格。观察代码可以看到,最终,表格种数据会被存放进一个叫EnemiesList的字典,字典类型为<int,string>,分别对应表格每一行(除首行外)的编号,和每一行(除编号外)的文本信息。

        然后赋值方法就是根据编号得到文本信息,拆分文本信息并分析赋值。

string[] s3 = enemyDetail.Split(",");

//怪物细节信息 格式: 怪物等级,怪物名称,怪物最大生命值,怪物伤害,怪物移动速度,怪物掉落金钱,怪物掉落经验,怪物攻击间隔(远程)
try
{
    enemy.GetComponent<Enemy>().Lv = int.Parse(s3[0]);
    enemy.GetComponent<Enemy>().MaxHP = float.Parse(s3[2]);
    enemy.GetComponent<Enemy>().Damage = float.Parse(s3[3]);
    enemy.GetComponent<Enemy>().MoveSpeed = float.Parse(s3[4]);
    enemy.GetComponent<Enemy>().DropMoney = int.Parse(s3[5]);
    enemy.GetComponent<Enemy>().DropEXP = int.Parse(s3[6]);
}
catch { }

上面是拆分赋值的例子。 

        通过上述一系列操作,让不同种类的敌人,武器,物品等等以数据的方式流通,而不是以一个实例的方式,只有在需要创建它们的实例时,才将数据赋值给实例。这就是我在临时制作时的大体思路。

        比如我的装备,在某个列表或数组中,只是一个个int类型的编号,但是在商店中,通过Block脚本的自动更新,会通过获取装备列表里的各个编号,来显示编号对应的图像。

        敌人也是一样的,通过编号对应的信息,实例化对象并赋值。

现在时

        当然上面的都是过去式,因为这些是在GJ赶工的情况下逼出来的,代码之间和整个架构还是有很多问题的,比如:

        1.表格数据若是都暂存到字典,而且在上面的设计中,所有相关字典是在整个游戏程序开始时生成的,并且存在知道游戏关闭,等同于游戏过程全程占用到内存。如果数据非常多,会不会因此影响到内存?

        2.赋值过程,存在在很多时候,比如上面的敌人生成时要赋值,武器生成时要赋值,物品显示时要赋值。有没有更统一的管理方案?

        3.在第一次打包中,表格数据并没有被一起打包,原因是Unity并没有将它们打包,所以需要放在一个可以被一起打包的路径。

将来时

        对于上面的第3个问题,目前已经有两种解决方案:

1.放在Resources文件夹里,随程序打包并加密。

2.放在StreamingAssets文件夹里,随程序打包但不加密,保留原样。

事实上,这两文件夹也是目前唯二会被Unity打包的文件夹。可以按需选择。

        对于其余两个问题,我还需要时间反复琢磨。

如果写路径的话,特别注意路径不能写死,因为每个平台的环境都不同。我就在一开始将路径写死,结果只能在Editor正常运行。

        其他问题的话有待解决吧。

总结

        这个项目让我感触最深的便是表格数据的重要性。表格真的是这种需要复杂数据的游戏的一大得力助手。也正因那几天的辛劳,让我觉得这种游戏不太适合GJ。它的优点和缺点一样。

        可能接下来的好几天,我都会围绕从各个角度优化这个项目做文章记录。

        有任何意见,建议,分享,思路,提示,构思,修正,批评,欢迎致信。

  • 19
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Moweiii

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值