任务系统的设计

(2024.11.29)

先来看看我是如何设计满足需要的结构体的。


struct ConNode { //对话结构体
	int fight;// 会不会在这句话后进入战斗,如果进入则为敌群编号
	int fight_continue;//战斗失败后是否继续剧情,如果继续的话会将玩家回复到满状态
	char content[10000];//这句话的内容
	int ifchoose;//是否需要选择,以及选择的数量
	char chooselist[5][100];//每个选择的内容
	int next[5];//对应应跳转的节点
	int start_task;//这句话是否会开启某个任务,任务的编号。无则为0.
	int check_task;//这句话检查某个任务是否完成
	int finish_next_con[2];//在检查玩任务是否完成后,依据情况触发不同的对话

};

struct Task {
	char name[100];
	int see;//是否会在任务列表里看见(用于多分支结局的任务给出不同的奖励)
	int npc_s;//这个任务找谁接取
	int npc_f;//这个任务找谁提交
	int con_s;//接取这个任务第一句话是什么(当这个任务可被接取时,将上述npc的对话节点改成这个
	int con_f;//提交这个任务第一句话是什么
	int start;//判断这个任务有没有开始(指有没有被接取)
	int finish;//判断这个任务有没有完成(有没有被提交,被提交的任务不会被再次检查)
	int father;//该任务是否是某个大型任务的子任务(用于输出任务树时不会重复输出子任务以及完成子任务时检查父任务是否完成)
	int sonnum;//这个任务有几个子任务
	int son[5];//子任务的编号
	int condition[4];//前置条件,0是否拥有某种道具,1是否拥有某种纪念品,
	//2是否完成某个任务,3对应0所需的道具数量
	long lever;//前置条件-是否达到指定等级
	char intro[1000];//任务内容描述
	int f_son;//完成条件,至少要完成几个子任务(用于多分支任务)
	int f_condition[4];//完成条件,0是否拥有某种道具,1是否拥有某种纪念品,
	//2无作用,3对应0所需的道具数量
	int give_prop;//任务完成给予什么道具
	int give_prop_num;//数量
	int give_souvenir;//纪念品奖励
	int give_exp[LEN];//经验奖励
	int give_gold[LEN];//金币奖励
	int next;//后续任务的编号,没有则为0(大型任务在接取时自动开启子任务,这个变量只用于任务链传递)
};

struct NPC {
	char name[50];
	int task[51];//记录这个npc会受到哪些任务影响
	int var[50];//独立变量
	
	int nomal_con;//在没有任务的情况下这个NPC的对话起点
	int con;//对话的起点,该内容是一个对话节点,可能会因为任务没完成造成用语的改变,
};

嗯……可能有些繁琐。我们还是回到思考一个任务系统应该满足什么需要的问题上来吧。

对于一个游戏,尤其是RPG游戏来说,任务无疑是推动剧情发展、提供游戏引导不可或缺的一个表现形式(当然,仍有一些不需任务系统就可展现良好剧情的游戏,如去月球等),也因为这一点,如何设计任务、如何用任务来引导玩家进行特定操作就成了设计师们头疼的问题。对于这一点,这里有一些很好的文章来阐述:

https://necromanov.wordpress.com/2010/11/14/quest-design-01/
https://necromanov.wordpress.com/2010/11/23/quest_design_2/
https://necromanov.wordpress.com/2010/12/03/quest_design_03/
https://necromanov.wordpress.com/2010/12/14/quest_design_04/

https://www.zhihu.com/question/40292677 (貌似知乎页面无法被嵌入)

上面这些页面虽然不能解决如何设计任务的难题,但至少让我们这种门外汉知道该怎么做。

但笔者在这里要说的不是具体的任务内容,而是任务的载体——任务系统。


回到最初的问题:任务是用来做什么的?

“任务可以指引玩家游玩方向。任务可以给予玩家奖励。任务可以串联剧情。任务也可以改变剧情或被剧情所改变。”

“任务应当是有条件的,包括接取条件、完成条件。”

这是我总结出来的一些要素。它们还不是很全面,但我会补完的(确信)。

现在我们从一个任务的流程来审视这些要素,我是说,大部分任务会有的流程。

首先是接取任务。任务应当如何接取?是通过与npc对话,还是玩家到达某个地方自动触发?在我的游戏中,由于一切战斗外的文本都是以对话的形式进行的,因此我在对话结构体中设置了可以开启任务、检查任务的功能。(这有点像极乐迪斯科,除了noc会与玩家对话,系统在触发事件时也是以对话的形式展现的。)而任务结构体里的npc_s等变量,则是为了更广泛、更一般的任务接取设置的。


知道如何接取后,还应当思考玩家能不能接取的问题。你也不想玩家在游戏开始就完成了某个后期的跑腿任务,导致获得逆天道具、游戏瞬间索然无味吧?这就是了。设置合适的接取条件是控制任务流程的方式之一:如果我把接取NPC设置为只有后期才会出现的剧情人物,或者干脆一点,玩家只有达到某个等级、完成某个前置任务才能触发接取对话,那这些问题遍迎刃而解了。

对于道具和纪念品的解释

可以看到,任务结构体里大部分变量全部用于记录接取条件。这里我觉得我有必要解释一下条件中的道具和纪念品是什么意思:你接了一个跑腿任务,村民给你物品A让你送到C处,这就是任务道具。你到达村民B的位置后,系统对你判定是否携带任务道具,可能会触发村民B想要抢夺的任务,这就是任务的触发条件。而你送到C处后,系统也会进行一次判定,结算任务奖励。至于纪念品……就像它的名字一样,这是玩家完成某一重大剧情后系统给予玩家用于纪念的东西,比如你打败魔王获得的断角啦、你第一次走出家门母亲给你的护符啦,通过查询纪念品的有无,可以很方便的知道玩家的任务进行到了哪一步,玩家与某个NPC的关系是怎样的。


然后是如何完成任务。如果我需要设置一个杀怪的任务,我应该怎么做呢?在外面的系统中,你应该创建一个怪物,它每次被杀都会百分百掉落某件道具,然后判断道具的数量……我承认这有些繁琐。正常模式是记录接任务时这个怪的杀敌数,然后比较才是,好吧,我会改的。

但不仅如此。以上面跑腿的任务为例:触发B的抢夺任务后任务的完成形式就发生了改变:给B或给C。不论玩家选择哪个,这个任务都应该被判断完成,而不会出现未能满足初始条件导致的任务一直呆在玩家代办列表中。

结构体中的“子任务”就是为此设置的,它不仅可以用于帮助玩家把一个大型任务拆分成几个小任务,也可以与f_son结合以实现多分支任务。


最后是任务奖励,这可以说是最激动人心的部分了,哪怕你给玩家一个它们在战斗中完全用不上的东西,他们也会欣喜若狂的在背包中仔细鉴赏它(尤其是在这个任务是以剧情为主的情况下)不过这部分也没什么好说的,你可以给一个任务道具,用于接取某个被你藏在新手村的隐藏任务;也可以给一瓶回复药,然后接受玩家对你族谱亲切的问候。


说了这么多,我能说的也差不多说完了。下面看代码吧。

int checktask(struct Task *task) {
//检查任务函数,检查任务是否满足开始条件(返回1/0)、
//					任务是否开启(返回2)、
//					任务是否完成(返回3)
//					任务是否结束(返回4)
//优先级从前往后升高

	int result=1;
	if(task->finish) {
		result=4;
		goto END;
	}
	if(task->start) {
		result=2;
		goto FINISH;
	}


	//检查开启条件
	//检查道具
	struct Prop *task_prop;

	task_prop=index2prop(task->condition[0]);

	if(task_prop->havenum<task->condition[3])result=0;

	//检查纪念品
	if(!souvenir[task->condition[1]].have)result=0;
	//检查任务
	if(!tasks[task->condition[2]].finish)result=0;
FINISH://检查是否完成
	if(result==2) {
		result=3;
		//子任务是否全部完成
		if(task->sonnum) {
			int f_sonnum=0;//记录完成了几个子任务
			for(int i=0; i<task->sonnum; i++) {
				if(checktask(&tasks[task->son[i]])==3)f_sonnum+=1;
			}
			if(f_sonnum<task->f_son)result=2;
		}
		//道具

		task_prop=index2prop(task->f_condition[0]);

		if(task_prop->havenum<task->f_condition[3])result=2;
		//纪念品
		if(!souvenir[task->condition[1]].have)result=2;

	}


END:
	return result;
}

首先是免责声明:我用了很多goto语句(这在战斗函数中尤为突出),是因为我可以用它们免去很多不必要的条件判断,并且我对标签起的名字也不会造成程序的阅读困难。

void tasktree(void) {


	for(int i=1; i<500; i++) {
		struct Task task=tasks[i];
		if(task.father)continue;//如果这个任务是一个子任务则跳过
		if(task.start&&!task.finish) { //检查任务是否开始以及是否完成

			COLOR(15);
			SlowDisplay("||--",1);
			SlowDisplay(task.name,1);
			SlowDisplay("\n|     |--",1);
			COLOR(7);
			SlowDisplay(task.intro,1);
			printf("\n");
			COLOR(15);
			if(task.sonnum) {
				for(int i=0; i<task.sonnum; i++) {
					struct Task sontask=tasks[task.son[i]];
					if(sontask.start&&sontask.see) { //检查任务是否开始以及是否可见

						COLOR(15);
						if(sontask.finish)COLOR(8);
						SlowDisplay("|  ||--",1);
						SlowDisplay(sontask.name,1);
						SlowDisplay("\n|  |     |--",1);
						COLOR(7);
						SlowDisplay(sontask.intro,1);
						printf("\n");
						COLOR(15);
					}
				}
			}

		}
	}
	COLOR(7);
	wait();
	system("cls");
}

这里补充一下COLOR:

define COLOR(X) SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), X)

https://cdn.nlark.com/yuque/0/2023/png/40561973/1699943982605-382e2d02-eb06-46f3-a7ac-f2e4bd347850.png?x-oss-process=image%2Fformat%2Cwebp%2Fresize%2Cw_1147%2Climit_0

void check_npc_task(struct NPC *npc) {
	//检查该npc所有相关任务是否完成,并对该npc的对话起点做出相应修改。
	int tnum=npc->task[0];//读取这个NPC有几个任务
	for(int i=1; i<=tnum; i++) {
		int num=npc->task[i];//记录每个任务的编号
		int kg=checktask(&tasks[num]);//检查这些任务完成情况

		if(kg==1) { //该任务可被开启
			npcs[tasks[num].npc_s].con=tasks[num].con_s;//更改对话起点
		}
		if(kg==2||kg==3) {//任务已经开启或已经完成,但未提交
			npcs[tasks[num].npc_f].con=tasks[num].con_f;//更改对话起点
		}
	}
}


评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注