C++算法:有向无环图实现游戏技能树

news2024/11/15 23:29:51

文章目录

  • 前言
  • 一、游戏技能树的逻辑
  • 二、实现代码
    • 1、建立图
    • 2、各种方法函数
      • (1)、出度入度表生成方法
      • (2)、读取技能点
      • (3)、修改技能点
      • (4)、拓扑排序
    • 3、测试代码
  • 总结


前言

前面文章图结构入门提到了图结构的两种存储方式,但没有代码演示。这篇就用一个简单示例来学习一下有向无环图的具体应用,图的应用比较广泛,本文就简单实现一个游戏的技能树。别看游戏技能树叫树,实际上它多半是用有向图实现,类似科技树也一样。说明白了它也很简单,就是一个拓扑排序问题。


提示:以下是本篇文章正文内容,下面案例可供参考

一、游戏技能树的逻辑

玩过游戏的都知道技能树、科技树,各技能要点亮是有前置条件的。那么它是怎么实现的呢?逻辑问题嘛~ if else ... 多写几条就好了?显然不可能的哈,这么写的话,主管连代码带电脑一起给你砸丢出去。而且,游戏角色往往很多,每个角色的技能又是不同的,要是有几十个角色怎么办?

这其实是可以用有向图来解决,每个顶点代表一个技能,边就是逻辑关系,有入度的顶点就是有前置条件的,没有入度的就是初始技能,没有出度的就是终极技能了。它可以同时存在多个初始技能和终极技能,也可以存在既没有入度也没有出度的独立技能。
在这里插入图片描述

我们用一张图来表示,0和3是初始技能。与它们连接的技能都需要0和3先点亮这个条件才能点。2是独立技能,像这种独立技能也可以通过任务、克金什么的办法独立得到。具体游戏逻辑那是策划的事了~。本文主要是以实现这个技能树的逻辑为目的。我们对这个有向图拓扑排序后,可以得到下图:
在这里插入图片描述
拓扑排序可以有多种可能,这只是其中一个可能,只要没有倒着走的箭头,都是符合规则的拓扑排序。具体排成什么样子也不影响这个技能树的逻辑。

从下面这张图来看这个逻辑就简单明了多了,2技能是独立的,通过克金或做任务方式可以学会,这个技能不影响整个技能树的学习逻辑。0开始的这条技能线可以是出生就带的,3这条技能线可以是从某种途径学来的,学会3后才可以学6和7这种中高级技能,当然也可以是克金才能学的!0是出生就带的技能,它后面也可以一个个点下去,最后学会终极技能8。就是线路长了点,且学不了3和6。走3这种克金技能线可以直接得到6和7这种强力技能后回头学0技能线的技能。走克肝路线的得从0到1慢慢点,中间也设置了跳级路线,但跳级就丢了5或4这种中高级技能,而走克金路线后回头来学的可以从0先跳级去学4和8,再回头来补,反正克金的已经先一步学了6和7技能,不差这一个5嘛~,整个目标就是得让想以肝补金的玩家痛苦到去克金。

搞明白这个为了克金而生的技能树逻辑,就可以来实现它了。我们定义-1为灰色不可以点的技能,0为可以点亮的(只要有可用的技能点)。1-9就是技能已点的等级。

二、实现代码

前面说过,图的存储方式也有数组和链表,这里就以数组来实现,技能点的图元素很少,数组会方便些。先按上图(策划给的)定义好逻辑关系数组:int arr[11][2] = {{0,1},{0,4},{1,5},{5,4},{4,7},{4,8},{3,6},{6,7},{8,7},{5,8},{2}}; 这里只以一个角色的技能为例,具体实现肯定涉及多个角色,以及数据库的读写问题。我们这里只实现能用于多个角色的通用逻辑代码。

1、建立图

代码如下(示例):

#include <iostream>
#include <vector>

using namespace std;    //实际肯定不是std哈,这么写要哭的

class Graph{
    private:
        int vertex, idx; //顶点数、顶点最大下标
        int** matrix;    //有向图关系矩阵
        int* visited;    //存储是否已访问
        int* indegree;   //存储入度
        int* outdegree;  //存储出度
        int* skill;      //存储技能等级

    public:
        int* toposort;   //存储拓扑结构,演示用,写在这方便
        Graph(const int n ,int arr[][2]){
            vertex = n;
            idx = vertex - 1;
            visited = new int[vertex];           
            toposort = new int[vertex];          
            indegree = new int[vertex];          
            outdegree = new int[vertex]; 
            skill = new int[vertex];        
            matrix = new int* [vertex];          //生成有向图关系矩阵
            for (int i = 0; i < vertex; ++i){
                matrix[i] = new int[vertex];
                for (int j=0; j<vertex; j++){
                    matrix[i][j] = 0;
                }
            }
            for (int i=0; i<11; ++i){          //生成有向图关系,0为不连接,1为有连接,这个11是偷懒了,实际得计算数组长度,也可以用迭代器
                matrix[arr[i][0]][arr[i][1]] = 1;
            }
            matrix[2][0] = 0;  //补刀,2是孤立的顶点,因为{2}数组默认是{2,0},不补会变成[2][0]=1

            for (int i=0; i<vertex; i++){   //生成出度、入度表
                find_indegree(i);
                find_outdegree(i);
            }

            for (int i=1; i<vertex; ++i){          //生成技能初始表,-1是灰色不可点
                skill[i] = -1;
            }
            skill[0] = 1;                          //出生自带的技能
            dfs_modify_skill(0);                  //深搜改变关联技能的状态

            int id = 0;                    //拓扑排序独立的顶点,示例中的2技能
            for (int i=0; i<vertex; i++){
                if (!indegree[i] && !outdegree[i]){
                     toposort[id] = i;
                     id++;
                }
            }
        }

        ~Graph(){
            delete[] toposort;
            delete[] outdegree;
            delete[] indegree;
            delete[] visited;
            delete[] matrix;
            delete[] skill;
        }

虽然拓扑排序是一件简单的事,有多种方法可以实现,比如从入度为0的顶点搜起,搜到一个就剔除并记录到排序结果的前面,并将与这个点有关的顶点的入度减1,一直循环即可。也可以从出度为0的终点开始,搜索到一个出度为0的就剔除并记录到排序结果的后面,并将与它有关的边都删除,一直循环也可以。我们这里将采用深度优先的搜索方式,比较方便。

这个图生成是很简单的,就几行代码,这里主要是构造函数中其它逻辑比较复杂,再简单的算法,实际用到工作中,都会多出很多代码,主要是各种判断和生成初始值。这里还省略了很多关系不大的判断,比如判断传入的值是否合法,参数的数组长度直接写11等。

对前面的示例图来说,我们传入 arr 参数,构造出来的 matrix 矩阵就是关系图了。其中相应值为 1 的就是存在相应方向的边,我们可以打印出来看一下:

0 1 0 0 1 0 0 0 0 
0 0 0 0 0 1 0 0 0 
0 0 0 0 0 0 0 0 0 
0 0 0 0 0 0 1 0 0 
0 0 0 0 0 0 0 1 1 
0 0 0 0 1 0 0 0 1 
0 0 0 0 0 0 0 1 0 
0 0 0 0 0 0 0 0 0 
0 0 0 0 0 0 0 1 0

图中[0][1]的值是1,代表顶点0到顶点1有一条边,以此类推。

这里代码将我们图中的2技能排到了最前面,将0技能初始化成1,再想让玩家克,也得让玩家有个技能用是吧~,需要注意的是:我们拓扑排序部分的内容在这个技能树的实现中并没有实用意义,主要是为演示而写的,skill 数组存储的技能状态是按0-8的顺序存储的,和拓扑排序的顺序无关,读代码时要注意。

2、各种方法函数

以下就是各种为实现技能术操作的方法,主要有出度、入度相关的;更改技能点数的,读取技能点数的,以及搜索修改相关状态的方法等,下面一个个介绍:

(1)、出度入度表生成方法


        void find_indegree(int v){               //生成入度表
            for (int i=0; i<vertex; ++i){
                if (matrix[i][v]) indegree[v]++; 
            }
        }

        void find_outdegree(int v){             //生成出度表
            for (int i=0; i<vertex; ++i){
                if (matrix[v][i]) outdegree[v]++; 
            }
        }

这两个函数就是生成出度和入度表的,分别存储在两个数组中,看变量名就能明白。入度其实就是统计顶点所在的列有没有1。出度就是顶点所在的行有没有1。与上面打矩阵打印表对照一下就明白了。比如第0列中全是0,代表了顶点0没有入度,在技能树中,技能0没有前置条件,所以我们设定成出生就是1级。同样第三列也全是0,这个我们策划说是要让玩家克金才能解锁的,等着负责克金的兄弟来调用我们的方法解锁就是了。

(2)、读取技能点

        vector<int> read_skill(){    //读技能点
            vector<int> tmp;
            for (size_t i=0; i<vertex; ++i){
                tmp.push_back(skill[i]);
            }
            return tmp;
        }   

我们得给负责前端实现的兄弟一个方法读取技能点,让玩家在屏幕上爽歪歪的看着他(她)努力克金或克肝的成果。明显不能直接把 skill 数组给前端,要防着点前端兄弟,万一前端的兄弟给你写个奇怪的功能呢?当然这里skill是私有的,我们也给不出去。总不至于为这个写友元。所以我们定义一个向量,丢给前端兄弟去折腾。

(3)、修改技能点

        bool modify_skill(int v){   //加点,省略判断是否合法
            skill[v]++;
            dfs_modify_skill(v);
            return 1;
        }

        void dfs_modify_skill(int v, int first=1){   //深搜改变关联技能的状态
            if (first){
                for (int i=0; i<vertex; ++i) visited[i] = 0;
            } 
            if (visited[v]) return;
                visited[v] = 1;
                for (int i=0; i<vertex; ++i){
                    if (matrix[v][i]){
                        if (skill[i] > 0) dfs_modify_skill(i, 0);
                        else{
                            skill[i] = 0;
                            continue;
                        }
                    } 
                }  
        }

这里我们省略了判断过程,直接给加点了。dfs_modify_skill 方法是用深度搜索的方法去修改关联技能的状态,比如从-1修改到0。我们判断前置技能是不是已经点亮了为1以上了,根据这点递归判断与这技能相关的技能是否可以修改成0,让前端兄弟把它显示成白色或别的允许加点的颜色。if (skill[i] > 0) dfs_modify_skill(i, 0);这一句就是不断搜索,else 就是搜到关联技能了。嗯,这里有个小技巧:因为还有一个dfs函数也会用到visited这个数组,且每次搜索都是要先归0 的。所以用之前要归0,但是在递归时又不能每次归0,所以定义了一个默认参数来解决这个问题。

(4)、拓扑排序


        void dfs(int v){            //深搜拓扑排序,演示用,和逻辑无关
            if (visited[v]) return;  
            visited[v] = 1;
            for (int i=0; i<vertex; i++){
                if (matrix[v][i]) dfs(i);
            }
            toposort[idx--] = v;
        }

        void get_topo(){                //拓扑排序调用,演示用,和逻辑无关
            for (int i=0; i<vertex; ++i) visited[i] = 0;   //在这里将visited归0
            for (int i=0; i<vertex; ++i){
                if (!indegree[i] && outdegree[i]){
                    dfs(i);
                }
            }
        }

        void show(){            //显示矩阵,演示用,和逻辑无关
            for (int i=0; i<9; ++i){
                for (int j=0; j<9; ++j){
                    cout << matrix[i][j] << " ";
                }
                cout << endl;
            }
        }
        
};

这一部分和技能树的逻辑实现没有关系,dfs 深度优先搜索用于拓扑排序的实现,因为可能存在多个入度为0的顶点,所以又写了一个get_topo,这个函数判断顶点的入度是否为0,且有出度,从这种顶点开始搜索。至于没有入度也没有出度的技能顶点在构造函数中就已经解决了。if (visited[v]) return; 这句判断有向图是否存在环路,因为搜索之前有将visited清0的代码,如果在搜索过程中回到了出发点,肯定就是有环的。排序的结果是:

2 3 6 0 1 5 4 8 7 

因为2在构造函数中就已经排在了第一个。

3、测试代码

int main(){
    int arr[11][2] = {{0,1},{0,4},{1,5},{5,4},{4,7},{4,8},{3,6},{6,7},{8,7},{5,8},{2}};
    Graph t(9,arr);
    t.show();
    t.get_topo();
    for (int i=0; i<9; i++) cout << t.toposort[i] << " ";
    cout << endl;

    t.modify_skill(1);
    t.modify_skill(3);
    t.modify_skill(3);
    t.modify_skill(6);
    t.modify_skill(6);

    vector<int> tmp = t.read_skill();
    for (int i=0; i<9; i++) cout << tmp[i] << " ";

}

主要是修改技能点方法,和读取方法,其它都不太重要。这里技能3是为克金而生的,所以得解锁一次,我们直接用加一次点的方法给它加到0,再加一次才会是1,而其它技能是满足条件自动变0。这么运行后的技能树状态是:1 1 -1 1 0 0 2 0 -1 ,它是从0到8排序的。明显这是克金玩家嘛~


总结

本文只是一个极简单游戏技能树实现,演示了主要的实现方法。省略了大量的交互逻辑 ,条件判断,甚至连技能上限都没有检查。本文主要还是为了学习有向无环图的算法。希望各位读者玩家努力克金克肝~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/623730.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Vue】三:Vue组件:props配置 父组件获取子组件

文章目录 1.props配置1.1 方式一&#xff1a;简单的接收方式&#xff1a;直接采用数组接收1.2 方式二&#xff1a;添加类型限制1.3 方法三&#xff1a;添加类型限制&#xff0c;添加默认值&#xff0c;添加必要性1.4 不要修改props中的值 2.从父组件获取子组件 1.props配置 父…

Vulkan Tutorial 0 引言

1 开发环境 在这一章中&#xff0c;我们将设置您开发Vulkan应用程序的环境&#xff0c;并安装一些有用的库。除了编译器之外&#xff0c;我们将使用的所有工具都与Windows、Linux和MacOS兼容&#xff0c;但安装它们的步骤有些不同&#xff0c;这就是为什么它们在这里被单独描述…

MySQL所有基本操作详解

一.MySQL的基本操作 首先sql操作中的关键字的是大小写不敏感的&#xff0c;create 和CREATE是一样的。 1.库操作 1. 1查看数据库 show databases;show 和databases 之间有一个或者多个空格注意是databases而不是database结尾分号是英文形式&#xff0c;分号在SQL中是表示一…

爆肝3月斩获字节Java后端Offer,分享下我的面试复盘心得

前言 我背景是NUS计算机硕士&#xff0c;武汉理工EE本科&#xff0c;春招收获腾讯、字节等后端实习Offer&#xff0c;目前仍在字节实习&#xff0c;今天给大家分享一下面试准备经验&#xff0c;我认为也同样适用于大家日常的学习。 在开始分享之前&#xff0c;我想给大家抛出…

C语言 socket学习整理

分三个topic来熟悉C语言的socket使用方法。 一台client与一台server之间的双向TCP通讯。使用select接口实现的多台client与一台server之间的通讯。使用epoll接口实现的多台client与一台server之间的通讯。 TCP通信模型与UDP通信模型的区别 UDP通信模型中&#xff0c;在通信开…

uniapp-微信公众号静默授权

第一次开发公众号&#xff0c;以为静默授权非常的复杂&#xff0c;后面才发现静默授权本质上就是跳一个微信的内部链接&#xff0c;拿到code&#xff0c;通过code再去获取openid即可。 在网上找到了一个比较详细的教程进行了一些改造 首先跳转静默授权的地址&#xff1a; htt…

人体检测技术之TOF

人体检测技术之TOF 1.概述 智能人脸/视频锁领域的人体检测需求是要求远距离达到1m左右即可,一旦在此距离内检测人,则锁唤醒进行人脸识别,视频录制等操作。所以,人体检测技术非常关键。 选型主要是几个维度: 1.支持检测的距离范围,能否准确输出距离信息 2.支持检测FO…

100天涨薪4k!从功能测试到自动化测试,我整理的超全学习指南!

今年3月份&#xff0c;由于经济压力让我下定决心进阶自动化测试&#xff0c;已经24的我做了3年功能测试&#xff0c;坐标广州薪资定格在8k&#xff0c;可能是生活过的太安逸&#xff0c;觉得8000的工资也够了&#xff0c;但是生活总是多变的&#xff0c;女朋友的突然怀孕&#…

想让ChatGPT和低代码开发实现完美结合?看这篇文章就行!

关于ChatGPT ChatGPT&#xff0c;一款代表着目前人工智能最高水平的产物&#xff0c;自问世开始就饱受期待。这款由OpenAI所开发出来的大型语言模型&#xff0c;使用GPT-4技术来实现。其基于预训练数据集&#xff0c;可以进行自动聊天、自动生成各种文本等&#xff0c;被广泛应…

SpringBoot配置加载顺序

目录 前言一、Spring Boot 配置优先级二、命令行参数三、示例分析 前言 Spring Boot 不仅可以通过配置文件进行配置&#xff0c;还可以通过环境变量、命令行参数等多种形式进行配置。这些配置都可以让开发人员在不修改任何代码的前提下&#xff0c;直接将一套 Spring Boot 应用…

机器学习因子库及特征表达式,创业板布林带突破策略,年化13.3%(附代码)

原创文章第235篇&#xff0c;专注“个人成长与财富自由、世界运作的逻辑与投资"。 昨天有朋友留言说&#xff0c;L2R效果不好&#xff0c;当然我不知道这位兄弟的场景。 pybroker vs qlib pybroker的结构里&#xff0c;有一个特别的地方&#xff1a; 无论是规则型策略…

接口测试 —— 接口测试的意义

1、接口测试的意义&#xff08;优势&#xff09; &#xff08;1&#xff09;更早的发现问题&#xff1a; 不少的测试资料中强调&#xff0c;测试应该更早的介入到项目开发中&#xff0c;因为越早的发现bug&#xff0c;修复的成本越低。 然而功能测试必须要等到系统提供可测试…

GoogleTest之Matchers的用法

目录 组合匹配Matcher的类型转换重载函数的匹配基于参数执行不同的Actions多个参数作为一个整体匹配将Matchers作为谓词使用Matcher汇总 组合匹配 组合某些匹配某些值&#xff0c;可以使用以下&#xff1a; Matcher使用描述AllOf(m1, m2, …, mn)参数必须匹配m1, … mnAllOfA…

大数据:spark内核调度,DAG,job,宽窄依赖,stage阶段,内存计算管道,并行度task数量

大数据&#xff1a;spark内核调度 2022找工作是学历、能力和运气的超强结合体&#xff0c;遇到寒冬&#xff0c;大厂不招人&#xff0c;可能很多算法学生都得去找开发&#xff0c;测开 测开的话&#xff0c;你就得学数据库&#xff0c;sql&#xff0c;oracle&#xff0c;尤其s…

【测试开发】实训记录日志

软件测试系列文章目录 提示&#xff1a;这里可以添加系列文章的所有文章的目录&#xff0c;目录需要自己手动添加 例如&#xff1a;第一章 了解测试开发和软件测试 提示&#xff1a;写完文章后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 …

建模杂谈系列226 流程与对象

说明 鲁迅说&#xff1a;我家门前门前有两棵树&#xff0c;一棵是枣树&#xff0c;另一棵也是枣树。 从编程语言的角度&#xff0c;可以分为两大类&#xff08;面向过程或面向对象&#xff09;&#xff0c;可以参考这篇文章 文章的内容其实不多&#xff0c;我贴一下&#xff1…

spark入门 YARN模式(六)

一、背景 独立部署&#xff08;Standalone&#xff09;模式由 Spark 自身提供计算资源&#xff0c;无需其他框架提供资源。这 种方式降低了和其他第三方资源框架的耦合性&#xff0c;独立性非常强。但是你也要记住&#xff0c;Spark 主 要是计算框架&#xff0c;而不是资源调度…

CATIA软件各版本区别

之前有个朋友问catia v5r21和catia v5-6r 2011&#xff0c;有什么区别&#xff0c;是一样的吗&#xff1f; 众联亿诚才发现&#xff0c;很多朋友似乎看不懂CATIA各种版本各种型号&#xff0c;那么今天&#xff0c;众联亿诚就来详细解释一下&#xff0c;希望能帮助朋友们解答心…

苹果电脑磁盘诊断工具 SMART Utility for mac

SMART Utility for mac是一款Mac上磁盘诊断工具&#xff0c;能够自动检测磁盘的状态和错误情况&#xff0c;分析并提供错误报告,以直观的界面让用户可明确地知道自己的磁盘状况。SMART Utilitymac版支持普通硬盘HDD和固态硬盘SSD&#xff0c;能够显示出详细的磁盘信息&#xff…

版权中心改革之后,软著办理需要多少钱?

版权中心改革之后&#xff0c;软著办理需要多少钱&#xff1f; 众所周知版权中心2年改了3次&#xff0c;每次改革官费都会涨&#xff0c;最近一次是在4月份&#xff0c;官费涨了近一倍。 其实费用涨了还不算最坏的情况&#xff0c;主要是每次改革办理软著的难度都会增加很多。…