文章目录
- 前言
- 一、游戏技能树的逻辑
- 二、实现代码
- 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排序的。明显这是克金玩家嘛~
总结
本文只是一个极简单游戏技能树实现,演示了主要的实现方法。省略了大量的交互逻辑 ,条件判断,甚至连技能上限都没有检查。本文主要还是为了学习有向无环图的算法。希望各位读者玩家努力克金克肝~