体感应用网

 找回密码
 立即注册
查看: 110|回复: 6

用C#做一个控制台回合制游戏(1)

[复制链接]

1

主题

42

帖子

85

积分

注册会员

Rank: 2

积分
85
发表于 2023-4-19 17:55:06 | 显示全部楼层 |阅读模式
我的职业是一名关卡策划。从我进入游戏行业以来很幸运的可以以关卡策划作为我职业的起点,这几年的工作经历主要是负责PVE玩法的FPS游戏的关卡设计、战斗设计、玩法设计相关的内容。本身较少涉及到战斗系统、技能设计和伤害计算等偏系统和数值向的工作。不过好巧不巧,我本身非常喜欢与4x与回合制策略游戏,简单来说就是日本游戏界称为“大战略游戏“的类似于《英雄无敌》、《三国志》,《信长的野望》和《欧陆风云》、《钢铁雄心》这样的历史模拟游戏。一直以来希望能够做一款有自己IP的“大战略游戏”。由于在工作内容当中其实比较少能够接触到类似游戏的相关内容,而国内这类游戏的项目其实非常非常少。所以我开始在业余时间自学C#来尝试自己做游戏。
关于为什么要做一款控制台回合制游戏

大战略游戏经过接近30年的发展,绝大部分市面上的作品都遵循一个相对固定的范式,即本质上由两个大模块组成:大地图玩法小地图战斗
从光荣起家的信长野望、初代英雄无敌到现在的全战系列一直遵循这个范式。大地图玩法包含了4x类游戏所具备的经营、扩张、探索和发展等相关元素。而这所有要素的最终反馈都需要通过小地图战斗来进行,战斗也能进一步增加玩家在大地图扩张的能力。但同时两者从机制上又可以相对平行,市面上各种各样的大战略游戏在战斗规则上也各式各样。P社游戏遵循一套古典桌游式的投骰计算方法,全面战争的战斗真实模拟了两军对阵的过程,光荣游戏则演义式的突出武将的能力和特性。
因此我想到可以将战斗系统剥离出来,单独的进行玩法设计和验证。整个过程会更快,也可以更早的获得一些结果和反馈。因此我选择一边学习C#一边尝试开发一个这样的项目,同时也能巩固对代码的学习。
此外,很多年前在“独立游戏”这个称呼还没有如此流行的时候,我曾经玩过很多个人开发者做的ASCII界面游戏,包括著名的《矮人要塞》,CDDA,DCSS和TOME等,它们大部分都是传统old scholl roguelike游戏,专注与游戏性和内容的丰富度。因此用控制台来做游戏对我来说是挺自然的一件事情。
战斗设计原型

由于曾经非常沉迷于大战略游戏,而一度非常纳闷为什么国内没有厂商试图将这个品类移植到手游平台。去年和一个曾经做过SLG游戏的同事聊天,他非常推荐我去玩一下《三国志 战略版》。实话实说,我一直以来对于国内的SLG游戏观感不是特别正面,总感觉这类游戏和早期的ogame,Travian等页游类似,虽然披着4x游戏的皮,但本质是一个类似于贪玩蓝月那样课金就能当大哥的吸金陷阱。
一开始玩《三国志 战略版》只是抱着体验一下的态度去尝试,结果慢慢玩下去之后确实发现了这个游戏的很多可取之处。在这里就不再详细展开,有时间可以单开一篇文章讲一讲。这里主要想讲的是它的战斗系统。
作为几代宝可梦玩家,虽然pvp打的不多,但日月也打了几百层对战树的我来说。三战的战斗系统完全就是一个3v3自动战斗的宝可梦对战,甚至属性设计也可以与宝可梦一一对应起来。要知道宝可梦的系统可是经过了数十年的考验都没有大的变动,所以基于这个框架进行的设计,本身可玩性确实是有保障的。
在此之上,三战基于自动战斗这个前提对战斗确实进行了一定的调整,设计的整体性确实是比较高的。因此我便打算基于宝可梦3v3自动战斗这个框架来制作我的控制台策略游戏。
正式开始

那么现在就可以正式开始制作了。首先我们需要给游戏起个名字来充当项目名,为了简单好记我就先叫他《ConsoleTactic》,简称CT。


在VS里面把工程建好就可以进行开发了。
逻辑框架

在开始之前需要思考一下整个游戏的逻辑是怎样进行的的。
这时候总结一下我们知道的战斗规则:

  • 战斗的过程是两方用事先配置好的队伍进行战斗。
  • 每个队伍包含三个宝可梦(抽象为单位),其中一名为主将,每个单位可以有最多三个技能。
  • 战斗总共会进行8个回合,任何一方的主将在这8个回合内被击败(HP归零)则失败。如果8个回合结束时没有主将被击败则平局。
  • 每个回合中,所有的单位会按照速度顺序先后出手,每次出手随机选择一名敌方单位进行攻击。
从规则中可以归纳出几个模块
战斗过程模块:用于管理战斗的进程和相关的逻辑,比如回合,每个回合单位的出手顺序,胜利失败条件的判定等等
单位模块:用来管理单位,保存单位的数据和其他相关信息
技能模块:用于管理技能,技能的释放条件、判定等等相关信息
各种计算公式:比如伤害计算公式等等,这些通用的内容可以被单独抽离出来
下面就可以码代码了。首先我们肯定需要把整个游戏的主体逻辑写出来,保证程序能够运行。之后对功能进行扩展。
一开始,我们先设定两个单位,分别是玩家和怪物。双方有各自的HP和攻击力。每回合轮流进行攻击,直到一方死亡。
using System;

namespace ConsoleGame
{
    class Program
    {
        static void Main(string[] args)
        {
            // 初始化玩家和怪物的生命值和攻击力
            int playerHp = 100;
            int playerAtk = 10;
            double playerCritRate = 0.15; // 玩家暴击率15%
            int monsterHp = 200;
            int monsterBaseAtk = 5;
            int monsterAtk = monsterBaseAtk;

            bool playerTurn = true; // 初始化玩家先手

            while (playerHp > 0 && monsterHp > 0) // 当玩家和怪物的生命值均大于0时,继续游戏
            {
                if (playerTurn) // 玩家回合
                {
                    Console.WriteLine("玩家攻击!");
                    int damage = playerAtk;
                    Random rnd = new Random();
                    double rand = rnd.NextDouble();
                    if (rand < playerCritRate) // 如果触发暴击
                    {
                        Console.WriteLine("玩家造成了暴击!");
                        damage *= 2; // 伤害加倍
                    }
                    monsterHp -= damage;
                    Console.WriteLine("怪物受到了" + damage + "点伤害!");
                    Console.WriteLine("怪物剩余生命值:" + monsterHp);
                    playerTurn = false; // 轮到怪物回合
                }
                else // 怪物回合
                {
                    Console.WriteLine("怪物攻击!");
                    playerHp -= monsterAtk;
                    Console.WriteLine("玩家受到了" + monsterAtk + "点伤害!");
                    Console.WriteLine("玩家剩余生命值:" + playerHp);
                    monsterAtk += monsterBaseAtk; // 每次攻击后怪物攻击力增加5点
                    playerTurn = true; // 轮到玩家回合
                }
            }

            // 判断游戏结果
            if (playerHp <= 0)
            {
                Console.WriteLine("玩家死亡,游戏结束!");
            }
            else
            {
                Console.WriteLine("怪物死亡,游戏结束!");
            }

            Console.ReadLine();
        }
    }
}
这里的代码非常简单,甚至将规则描述给chatgpt之后就可以直接生成。
在主体完成之后。我们首先准备做下面几件事:
1.将单位的属性抽离出来,使得我们可以创建不同的单位并赋予他们独特的属性。
2.将攻击逻辑单独抽离出来,让怪物和玩家的攻击遵循同样的逻辑。为接下来单独管理伤害公式做准备。
3.为战斗过程添加回合数限制。
单位的属性抽离

进行单位属性抽离之前,首先要决定单位具有哪些属性,而单位的属性有哪些又取决于预期的战斗方式和战斗计算方法。这里就暂且参考一下宝可梦和三战,快速的把逻辑搭建起来。
首先参考一下宝可梦的单位属性:



图片摘自神奇宝贝百科

宝可梦的属性共有6种,HP、攻击、防御、特攻、特防。攻击和防御参与物理攻击计算,特攻和特防参与特殊攻击计算,速度决定行动顺序。


三战的属性与之类似,刨去政治和魅力这两个内政属性。武力=物攻,统帅=物防,智力=特攻+特防,速度不用说了。HP则由兵力值决定。
依据体感推算,这两个游戏的伤害计算公式应该都是减法公式,同时存在保底伤害(除非有特技影响不然伤害一般不会为0)。
那么如果我们仍然希望单位的特攻与特防分离的话,就可以简单的写出单位的逻辑:定义一个单位的基类,并定义每一种属性。当然这里还要增加一个字符串用来存放单位的名字。
public class unit
{
    //unit的基本属性
    public string name { get; set; }
    public int hitPoint;
    public int strength;
    public int speed;
    public int intelligence;
    public int defence;

    //unit的技能

}
虽然宝可梦中属性有着个体值、努力值和种族值的差异,但此时我们并不像做的这么复杂。那么这时候就可以建立不同的单位了,首先我们先建立一个模板单位,用来标定数值的参照系,这个单位有100点生命值和10点力量,速度为50,名字叫杂鱼。
class militia : unit
{
    public militia()
    {
        //属性
        name = "杂鱼";
        hitPoint = 100;
        strength = 10;
        speed = 50;
        intelligence = 10;
        defence = 0;
        //技能

    }
}
杂鱼创建好之后,我们就可以以此为参照设计各种不同的单位了。比如说我要设计一个叫做女骑士的单位,拥有更高的力量、生命值和护甲;以及一个叫做哥布林的单位拥有更少的生命值和更快的速度。
class ladyknight : unit
{
    public ladyknight()
    {
        //属性
        name = "ladyknight";
        hitPoint = 120;
        strength = 11;
        speed = 48;
        intelligence = 10;
        defence = 2;

        //技能

    }
}

class goblin : unit
{
    public goblin()
    {
        name = "goblin";
        hitPoint = 50;
        strength = 8;
        speed = 55;
        intelligence = 5;
        defence = 0;

        //技能

    }
}
因为伤害计算公式预计要遵循减法公式,因此单位的防御力平均值设计的要比力量低很多,避免经常打不出伤害或者只有保底伤害。
战斗逻辑抽离

首先我们知道,在轮到单位行动的时候,不仅仅会进行普通攻击,一些状态的结算,技能的发动判定都会在这个时候进行。因此这里在攻击以外显然应该有一个单独的逻辑层级用于处理这些内容。这里我们将它称为Action。
在Action过程当中,单位有可能进行攻击也有可能不进行(比方说遭到控制技能影响无法进行攻击)。那么显然应该由Action来判断是否要进行攻击。
因此这里我们应该建立两个方法。首先是Action方法,单位在每次轮到它行动时调用。其次是Attack方法,单位在行动的普通攻击阶段调用。
//行动方法
public static void Action (unit Actor)
{
    //主动技能

    //普通攻击
    Attack(unit attacker, unit defender);
}
//攻击方法

public static int attack(Attack(unit attacker, unit defender))
{
    if (unser.strength - reciver.defence > unser.strength * 0.1)
    {
        return unser.strength - reciver.defence;
    }
    else
    {
        return (int)Math.Ceiling(unser.strength / 10.0);
    }
}
在攻击方法里将攻击者和防御者作为输入,并进行伤害的计算。减法公式逻辑上是一个分段函数,当攻击者攻击力小于防御者防御力是我们给出攻击力10%的保底伤害,反之则用攻击力减去防御力得出伤害。
战斗过程重构

经过上述两个模块的修改,我们的战斗过程代码已经不能用了,需要依据新的逻辑来重写一遍。
首先,在战斗开始的时候需要添加一个准备阶段,在这个阶段内程序创建双方单位(这里我们暂时只做1v1进行验证后续再扩展为3v3或者nvn),并用来放置一些初始化的逻辑。
其次,回合数确定为最大8回合,超出时长判定为平局。
C#控制台工程默认由Main方法开始,因此我们需要将战斗过程也抽离出来,与Main方法解耦。
定义一个Battle方法,主体部分循环8次,每次是1个回合:
static void Battle(unit unit1, unit unit2)
        {
            for (int i = 0; i < 8; i++)
            {
                if (unit1.hitPoint > 0)
                {
                    Console.WriteLine("【第" + i + "回合】:");
                    Action(unit1, unit2);
                }
                else

                {
                    Console.WriteLine(unit1.name + "死亡!");
                    Console.WriteLine(unit2.name + "胜利!!");
                    break;
                }
                if (unit2.hitPoint > 0)
                {
                    Action(unit2, unit1);
                }
                else
                {
                    Console.WriteLine(unit2.name + "死亡!");
                    Console.WriteLine(unit1.name + "胜利!!");
                    break;
                }
            }
        }
速度与行动顺序的判定暂时还没加,暂时先让Unit1先出手。
定义一个初始化方法,未来用来放置一些需要在战斗开始前处理的逻辑:
        public static void Awake()
        {
            Console.WriteLine("————初始化");
        }
目前内容还什么都没有,但是让方法输出一行log,以便我们在运行时观察程序到底干了什么。
接下来修改Main方法的主体,初始化->创建单位->进行战斗:
static void Main(string[] args)
        {
            //初始化
           
            Awake();
            //主界面
            Console.WriteLine("Welcome to Console Tactice");

            //战斗准备
            //创建单位
            unit BlueUnit = new ladyknight();

            Console.WriteLine("————创建蓝方单位");


            unit RedUnit = new goblin();
            Console.WriteLine("————创建红方单位");

            Console.WriteLine("战斗开始!");
            Battle(BlueUnit, RedUnit);

        }
好了,这时候可以运行一下看看效果:



看来一个哥布林抓不住女骑士

看上去没什么问题。程序也能跑起来了,这时候可以随意添加各种单位来进行测试。
考虑到未来单位会很多,单独创建一个unit文件用来保存所有的unit数据。其他逻辑依然放在Program中。后续单位多了还需要创建一个外置的数据表格,将单位数据保存在csv或者json里面,避免每次修改都要重新编译。


那么第一阶段就到这里。后续还有很多地方可以优化,比如增加玩家自定义双方单位的功能、把nvn功能加上、将伤害公式独立出来、加入出手顺序判定等等。预期会将这个工程完成到一个可以玩的程度,并且加入一些我想要的设计,而不单单是复现宝可梦或者三站的战斗系统。
下一篇文章会讲一下如何实现技能系统,这部分也花了我比较多的时间,具体的更新时间就看我什么时候有空了。
回复

使用道具 举报

0

主题

1

帖子

0

积分

新手上路

Rank: 1

积分
0
发表于 2023-4-19 17:55:15 | 显示全部楼层
很棒
回复

使用道具 举报

0

主题

1

帖子

0

积分

新手上路

Rank: 1

积分
0
发表于 2023-4-19 17:56:00 | 显示全部楼层
谢谢夸奖[害羞]
回复

使用道具 举报

1

主题

42

帖子

84

积分

注册会员

Rank: 2

积分
84
发表于 2023-5-30 02:17:39 | 显示全部楼层
……
回复

使用道具 举报

0

主题

39

帖子

78

积分

注册会员

Rank: 2

积分
78
发表于 2023-7-10 09:39:27 | 显示全部楼层
路过
回复

使用道具 举报

0

主题

46

帖子

91

积分

注册会员

Rank: 2

积分
91
发表于 2023-8-1 12:35:27 | 显示全部楼层
我只是路过,不发表意见
回复

使用道具 举报

0

主题

41

帖子

82

积分

注册会员

Rank: 2

积分
82
发表于 2024-12-5 21:21:57 | 显示全部楼层
大人,此事必有蹊跷!
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

Archiver|手机版|小黑屋|体感应用网

GMT+8, 2025-3-15 09:16 , Processed in 0.833763 second(s), 23 queries .

Powered by Discuz! X3.4

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表