首页 > 代码库 > 游戏开发书籍《Visual C++ 冒险游戏程序设计(版本千寻)》源代码与书籍笔记

游戏开发书籍《Visual C++ 冒险游戏程序设计(版本千寻)》源代码与书籍笔记

这里放置了阅读版本千寻的《Visual C++ 冒险游戏程序设计》一书以及配套源码时,自己对书中以及源码的理解以及一些心得,大部分都是将心得以注释的形式写在配套源代码中;

 

//------------------------------------------------------------------------------------------------------------------------------------------

// @brief 下述代码描述了脚本编译器如何在一遍处理(1 Pass)之内同时收集与处理标签符号

//------------------------------------------------------------------------------------------------------------------------------------------ 

//
// 登录标签
//
void MakeScript::AddLabel(const char *label)
{
	// 该函数只有发现标签定义语句时才会被调用
	// 此时参数 label 指出正在定义的标签名

	// 遍历所有目前为止出现在脚本中的标签,这些标签中
	// 有些已经知道在哪里定义了,有些,则被复数个指令引用着,但是却不知道在哪里定义

	for (vector<Label>::iterator lp = labels.begin(); lp<labels.end(); ++lp)
	{
		if (lp->label == label) {			// 已经登录了

			// 如果标签的参考链为空,则该标签已经被定义

			if (lp->ref == NULL) {			// 标签已经定义了
				Error("label ‘%s‘ redefinition line %d and %d",
					label, lp->line, reader->GetLineNo());
			}
			else {							// 标签已经被参考了

				// 如果标签的参考链不为空,说明该标签被复数个指令引用着
				// 这里,我们遍历所有引用着该标签的指令,将这些指令的标签引用重新设置为
				// 该标签的定义位置(当前指令数量)

				// 这里除了清空标签的引用链之外,还要将标签从被引用重置为被定义

				LabelRef *chain = lp->ref;

				lp->line = reader->GetLineNo(); // 为该标签设置标签所在行数
				lp->ref = NULL; // 清空参考链
				lp->jmp_addr = ncommand; // 设置跳转目标(当前指令数量)

				while (chain != NULL) {		// 清除参考
					LabelRef *next = chain->next;
					// 将所有引用该标签的指令的跳转目标数据成员的值设置为该标签的跳转目标
					// 通常只有 if 和 goto 两个指令会引用标签
					*(chain->label_ref) = ncommand;
					delete chain;
					chain = next;
				}
			}
			return;
		}
	}

	// 登录新的标签
	// 这里登录的标签,所有都是已经定义的标签
	// 而没有被定义的标签,会在 FindLabel 函数中被加入

	labels.push_back(Label(label, reader->GetLineNo(), ncommand, 0));
}

//
// 参考标签
//
void MakeScript::FindLabel(const char *label, long *reference)
{
	// 该函数只有在发现 label 标签被引用时被调用
	// 参数 reference 保存了所有引用了 label 标签的指令的跳转目标数据成员的指针

	*reference = 0;

	for (vector<Label>::iterator lp = labels.begin(); lp<labels.end(); ++lp) {

		// 如果找到了 label 标签
		// 那么有两种情况,要么 label 标签已经被定义,此时直接将 label 标签的跳转目标
		// 填入到参数 reference 中
		// 要么 label 标签没有被定义,仅仅是被向前引用了,此时在 label 标签中新增引用 Item

		if (lp->label == label) {			// 已经登录了

			// 参考链中每个 Item,保存了所有引用了 label 标签的指令的跳转目标数据成员的指针

			if (lp->ref != NULL) {			// 标签有被参考

				// 新增在参考串列中
				lp->ref = new LabelRef(lp->ref, reference);
			}
			else {							// 已经登录了

				// 如果 lable 已经定义,则直接将 label 的跳转目标填入到参数 reference 中
				// FindLabel 函数只会被 if 指令以及 goto 指令调用
				// 也就是说,参数 reference 是 if 指令以及 goto 指令的跳转目标的数据成员的指针

				// 回填跳跃目标
				*reference = lp->jmp_addr;
			}
			return;
		}
	}

	// 如果没有找到 label 标签,说明 label 还没有被定义,只是被向前引用了
	// 这里增加的都是没有被定义的标签

	// 登录新的标签参考
	LabelRef *chain = new LabelRef(0, reference);
	labels.push_back(Label(label, reader->GetLineNo(), 0, chain));
}

 

//------------------------------------------------------------------------------------------------------------------------------------------

// @brief 下述代码描述了脚本播放器如何将脚本指令的执行逻辑与 Windows 消息循环进行一个非常精妙的协调

//------------------------------------------------------------------------------------------------------------------------------------------

//
// IDLE处理
//
BOOL CScriptAction::IdleAction()
{
	if (status == Continue) {				// “继续”执行

		// 先阻塞处理所有不需要与 Windows 界面产生交互的脚本命令
		// 比如 calc、set、if、goto 等语句
		// 直到处理到任何一条图形命令时,while 循环会退出
		// 因为图形命令需要向 Windows 发送图形相关的消息
		// 随后需要从 IdleAction 返回到消息循环,Windows 才有机会接受到消息反馈

		do {
			status = Step();				// 执行1步
		} while (status == Continue) ;		// 继续?

		// 当 status 为 WaitNextIdle 时,说明需要向 Windows 发送消息
		// 因此必须先退出掉当前的 IdleAction 函数,将执行机会交回给 Windows
		// 如果 IdleAction 一直运行不退出,那么 Windows 将没有任何机会接受任何消息
		// 因此,设置 status 为 Continue,然后返回 TRUE,告诉消息循环,如果你处理完消息了
		// 就要继续进行 OnIdle 的调用

		if (status == BreakGame) {			// 结束
			Abort();
		}
		else if (status == WaitNextIdle) {	// 等到下一次IDLE再进来
			status = Continue;				// 改为继续
			return TRUE;
		}
	}

	// 如果返回 FALSE,说明 IdleAction 不需要处理更多的事情了
	// 接下会一直执行 Windows 消息,直到没有任何消息处理为止,再继续调用 IdleAction
	// 在这里,如果 Step 函数返回 WaitKeyPressed 或者 WaitMenuDone 时
	// 因为接下来的操作需要与界面进行交互,比方说移动鼠标时判断鼠标是否落入菜单项
	// 或者显示文本时,需要等待玩家点击鼠标来跳到吓一跳消息,所以返回 FALSE
	// 让消息循环有机会被执行

	// 这里如果返回了 FALSE,那么接下来,如果没有消息处理,消息循环会因为 WaitMessage 的原因
	// 一直阻塞,直到有任何消息产生,就会进行消息处理,如果所有消息处理完了
	// 就会调用 OnIdle 函数,再次进入到这里

	// 返回 TRUE 的话,该函数返回后,如果有消息要处理,处理完消息后,会进入该函数
	// 如果没有消息处理,也不会在消息循环中执行 WaitMessage 函数,消息循环不会被阻塞
	// OnIdle 函数很快就会得到执行机会

	return FALSE;
}

//
// 事件循环(Message Loop)
//
int CWinApp::Run()
{
	bool idle = true;
	long count = 0;

	if (MainWnd)
		MainWnd->ShowWindow(CmdShow);

	for (;;) {
		if (::PeekMessage(&msgCur, NULL, 0, 0, PM_NOREMOVE)) {
			if (!::GetMessage(&msgCur, NULL, 0, 0))
				return msgCur.wParam;

			if (!PreTranslateMessage(&msgCur)) {
				::TranslateMessage(&msgCur);
				::DispatchMessage(&msgCur);
			}
			idle = true;
			count = 0;
		}
		else if (idle) {
			if (!OnIdle(count++))
				idle = false;
		}
		else {
			::WaitMessage();
		}
	}
}

 

-- 2016-10-17 By NekoDev cnblogs  

-- 原创技术文章,转载请注明出处,并保证内容的完整性

  

游戏开发书籍《Visual C++ 冒险游戏程序设计(版本千寻)》源代码与书籍笔记