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


前言

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


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

一、游戏技能树的逻辑

玩过游戏的都知道技能树、科技树,各技能要点亮是有前置条件的。那么它是怎么实现的呢?逻辑问题嘛~ 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排序的。明显这是克金玩家嘛~


总结

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

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

无证的攻城狮

如本文对您有用,大爷给打个赏!

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

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

打赏作者

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

抵扣说明:

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

余额充值