easyx空洞武士项目总结

news2025/1/8 16:39:36

能帮到你的话,就给个赞吧 😘


文章目录

  • 前言
  • 0.游戏窗口
  • 1.游戏主循环
    • FPS
    • 主循环功能拆解——帧
      • 输入
      • 更新
      • 渲染
    • 完整的主程框架
      • easyx渲染问题
        • 拖影——cleardevice
        • 画面闪烁——批渲染
      • 完整主程
  • 2.Zero
    • 定义接口
    • 物理模拟
      • 闲置
      • 左右奔跑
      • 跳跃
      • 翻滚
    • 攻击
  • 不重要头文件
    • vector2.h

前言

  本项目是跟着up做的最后一个项目,因此,本文将对本项目及之前的项目有印象以及是重点的以及优化全部做总结,类似于重新开始写。因为隔了一段时间发现即便是照抄,当时明白了,但不是自己写的,过了一段时间依旧不明所以。因此,本文将以构造空洞武士场景以及武士零角色为目标,从0开始,梳理之前所有的项目,并对大的概念,重要框架,以及一些细小疑难进行讲解。
  本文的模式将类似与客服问答沟通,因为之前与客服交流,发现这种还挺好的。相信即便是0也能开始。
  武士零角色以zero代替。
  一些对讲解概念无关的自定义头文件在不重要头文件中

0.游戏窗口

#include <graphics.h>	//easyx头文件

int main() {

	//一个(1280, 720)的游戏窗口
		//游戏窗口:渲染所有的游戏内容
	initgraph(1280, 720);

	return 0;
}

easyx构建窗口非常简单,仅需包含一个头文件和一句话即可。

1.游戏主循环

然而,没有循环的话窗口将会立即退出,所以,在其后加一个死循环防止退出。
而这个死循环也就是游戏的主循环。

#include <graphics.h>

int main() {

	initgraph(1280, 720);

	while (1) {

	}

	return 0;
}

FPS

游戏通常还需要控制FPS。FPS就是一秒钟有多少帧。
控制FPS也很简单,只需要计算每一帧的时间,然后与预期时间相减休眠即可。
以144hz为例。

#include <graphics.h>
#include <chrono>
#include <thread>

using namespace std::chrono;
using namespace std::this_thread;

int main() {
	
	initgraph(1280, 720);

	const nanoseconds desiredFrameDuration{ 1000000000 / 144 };

	while (1) {
		//帧开始
		const steady_clock::time_point frameStartTime = steady_clock::now();
		
		//帧结束
		const nanoseconds frameDuration{ steady_clock::now() - frameStartTime };
		const nanoseconds idleDuration = desiredFrameDuration - frameDuration;
		if(idleDuration > nanoseconds{0})
			sleep_for(idleDuration);
	}

	return 0;
}

这样,即可实现稳定144hz刷新,同时,如果超时的话便不再延迟。

主循环功能拆解——帧

主循环负责实现游戏的所有功能。而这所有功能,其实就是每一帧。

#include <graphics.h>

int main() {

	initgraph(1280, 720);

	while (1) {

		//每一帧

	}

	return 0;
}

那么,一帧所做的事情通常分为三类
输入、更新、渲染。

#include <graphics.h>

int main() {

	initgraph(1280, 720);

	while (1) {
	//一帧

		//输入

		//更新

		//渲染
	}

	return 0;
}

即 一帧 = 输入 + 更新 + 渲染。

输入

在easyx中获取键盘及鼠标的输入仅需两行代码。

ExMessage msg;
//获取输入
if (peekmessage(&msg)) {
	
	//处理输入
	zero.processInput(msg);
	
}

peekmessage即可获取键盘以及鼠标的输入,存入到msg中。

更新

游戏中非静止不变化的对象,都需要提供一个更新的方法,同时呢,也都需要一个Δt参数,来模拟时间流动。
这与游戏的实现有关。游戏实现是离散的,我们固定了1秒钟144个帧,所以,我们需要计算更新与更新之间的时间,来模拟时间的连续。

//更新
static steady_clock::time_point lastUpdateTime = steady_clock::now();

const steady_clock::time_point currentUpdateTime = steady_clock::now();

const duration<float> deltaT{ currentUpdateTime - lastUpdateTime };

//更新前为t0
zero.update(deltaT.count());	//此时 t0 + Δt
//更新后为t1

lastUpdateTime = currentUpdateTime;

渲染

渲染则负责将更新好的数据绘制在窗口中。

zero.render();

在zero渲染前,

完整的主程框架

#include <graphics.h>
#include <chrono>
#include <thread>

using namespace std::chrono;
using namespace std::this_thread;

int main() {

	initgraph(1280, 720);

	const nanoseconds desiredFrameDuration{ 1000000000 / 144 };

	while (1) {
		
		const steady_clock::time_point frameStartTime = steady_clock::now();

		//输入
		ExMessage msg;
	
		if (peekmessage(&msg)) 
			zero.processInput(msg);
		
		//更新
		static steady_clock::time_point lastUpdateTime = steady_clock::now();

		const steady_clock::time_point currentUpdateTime = steady_clock::now();

		const duration<float> deltaT{ currentUpdateTime - lastUpdateTime };

		zero.update(deltaT.count());

		lastUpdateTime = currentUpdateTime;

		//渲染
		zero.render();

		const nanoseconds frameDuration{ steady_clock::now() - frameStartTime };
		const nanoseconds idleDuration = desiredFrameDuration - frameDuration;
		if (idleDuration > nanoseconds{ 0 })
			sleep_for(idleDuration);
	}

	return 0;
}

这样,便搭建好了144刷新的主程框架。
此种实现下zero.update的时间仅是模拟,并不严格对应。
例如玩家在t1帧按下移动键,t2帧松开移动键,那么移动的时间理应为t1与t2之间的帧间隔。
但实现中的时间却为 t0与t1之间的帧间隔。那是因为在按下的那一帧t1就已经开始运动了,但时间却是上一帧t0的间隔,而t2帧松开移动键后就不移动了,所以实际的运动时间是[t0, t1)。不过这并不影响游戏的连续模拟。

easyx渲染问题

拖影——cleardevice

initgraph开启一个窗口,
然而此时运行是有拖影的,是因为easyx窗口每一帧所渲染的画面需要手动清除,不清除的话依旧会留在窗口中。
easyx 清除渲染 也仅需一句话 cleardevice
放在渲染开始时调用即可

//渲染
cleardevice();
画面闪烁——批渲染

画面闪烁的原因是因为
easyx默认的渲染调用方式是一次渲染一次引擎调用,而cleardevice是将窗口涂黑,这样连续起来的话就是 黑 -> 画面 -> 黑 ->画面,就形成了视觉的闪烁。
解决方式也很简单 BeginBatchDraw即可。
BeginBatchDraw开启批渲染,直到调用FlushBatchDraw才进行一次性渲染。这样就减少了画面的闪烁。

#include <graphics.h>

int main() {
	
	//初始化窗口
	initgraph(1280, 720);

	BeginBatchDraw();

	while (1) {
		//渲染
		cleardevice();
		
		//一次性渲染
		FlushBatchDraw();
	}

	EndBatchDraw();
}

完整主程

#include <graphics.h>
#include <chrono>
#include <thread>

using namespace std::chrono;
using namespace std::this_thread;

int main() {

	initgraph(1280, 720);

	const nanoseconds desiredFrameDuration{ 1000000000 / 144 };

	BeginBatchDraw();

	while (1) {

		const steady_clock::time_point frameStartTime = steady_clock::now();

		//输入
		ExMessage msg;

		if (peekmessage(&msg))
			zero.processInput(msg);

		//更新
		static steady_clock::time_point lastUpdateTime = steady_clock::now();

		const steady_clock::time_point currentUpdateTime = steady_clock::now();

		const duration<float> deltaT{ currentUpdateTime - lastUpdateTime };

		zero.update(deltaT.count());

		lastUpdateTime = currentUpdateTime;

		//渲染
		cleardevice();

		zero.render();

		FlushBatchDraw();

		//刷新率
		const nanoseconds frameDuration{ steady_clock::now() - frameStartTime };
		const nanoseconds idleDuration = desiredFrameDuration - frameDuration;
		if (idleDuration > nanoseconds{ 0 })
			sleep_for(idleDuration);
	}

	EndBatchDraw();

	return 0;
}

看似很多,其实跟要实现的内容没啥关系,都是关于easyx的框架,我们精简一下也就是

#include <graphics.h>
#include <chrono>
#include <thread>

using namespace std::chrono;
using namespace std::this_thread;

int main() {

	initgraph(1280, 720);

	const nanoseconds desiredFrameDuration{ 1000000000 / 144 };

	BeginBatchDraw();

	while (1) {

		const steady_clock::time_point frameStartTime = steady_clock::now();

		//输入
		ExMessage msg;

		if (peekmessage(&msg)) {

		}
		

		//更新
		static steady_clock::time_point lastUpdateTime = steady_clock::now();

		const steady_clock::time_point currentUpdateTime = steady_clock::now();

		const duration<float> deltaT{ currentUpdateTime - lastUpdateTime };

		

		lastUpdateTime = currentUpdateTime;

		//渲染
		cleardevice();

	

		FlushBatchDraw();

		//刷新率
		const nanoseconds frameDuration{ steady_clock::now() - frameStartTime };
		const nanoseconds idleDuration = desiredFrameDuration - frameDuration;
		if (idleDuration > nanoseconds{ 0 })
			sleep_for(idleDuration);
	}

	EndBatchDraw();

	return 0;
}

接下来就跟easyx没什么关系了,实现独立的zero。

2.Zero

Zero的状态一共有七个
闲置 /奔跑 /跳起 /下降 /翻滚 /攻击 /死亡
然而这些都不重要,重要的是我们如何在一个游戏中设计一个类。
其实easyx什么都没做,只提供了一个黑屏窗口而已,而我们要做的,就是在这个二维坐标中使用数据去模拟一些物理而已。
注:easyx中的坐标系是右下的,所以我们以右下为正。

定义接口

class Zero {
public:
	void processInput(const ExMessage& msg) {
	}

	void update(float deltaT) {
	}

	void render() {
	}
};

物理模拟

如何实现物理这种宏大的目标呢,先从小事做起。

闲置

仅需一个坐标,即可模拟闲置

private:
	Vector2 zeroCoordinate{ 620, 340 };

这样,就已经完成了闲置。无论输入什么,Zero都不会动。

为了观察,使用质点来渲染Zero

void render() {
	fillcircle(zeroCoordinate.x, zeroCoordinate.y, 20);
}

左右奔跑

如何完成奔跑呢,仅需一个速度量即可。没错,完成物理的最重要两个变量也就是坐标和速度。其实就是初中物理而已。
当按下左右键,赋予奔跑速度即可。

private:
	bool isLeftKeyDown = false;
	bool isRightKeyDown = false;
	
	const float speedRun = 300;
	Vector2 zeroVelocity = { 0,0 };

这些,便是实现奔跑的所有变量了。

接着,我们仅需在奔跑时赋予奔跑速度即可。

void update(float deltaT) {

	//run
	if (isRightKeyDown - isLeftKeyDown)
		zeroVelocity.x = speedRun;

	//坐标更新
	zeroCoordinate += zeroVelocity * deltaT;
}

至此,奔跑逻辑便完成了。
运行(需要将processInput填完),就会发现许多bug。
这里的主要原因就是isRightKeyDown - isLeftKeyDown,这就导致了只能向右移动,向左移动的话就需要isLeftKeyDown - isRightKeyDown,但我们可以将这个式子的结果抽象为一个变量,即奔跑方向runDirection

private:
	int runDirection = 0;	//runDirection 需要正1负1,所以设为int
//run
runDirection = isRightKeyDown - isLeftKeyDown;
if(runDirection)
	zeroVelocity.x = runDirection * speedRun;

这样即可实现左右奔跑,运行,但会发现还有一个bug,就是不能停止。
仅需奔跑停止时赋值为0即可。

if (runDirection)
	zeroVelocity.x = runDirection * speedRun;
else
	zeroVelocity.x = 0;

完整代码如下

void update(float deltaT) {

	//run
	runDirection = isRightKeyDown - isLeftKeyDown;
	
	if (runDirection)
		zeroVelocity.x = runDirection * speedRun;
	else
		zeroVelocity.x = 0;

	//坐标更新
	zeroCoordinate += zeroVelocity * deltaT;
}

以下则是无关的按键处理,为了方便,在这里也将之后的按键一并给上。
按键处理负责按键按下时为真,按键松开时为假。

private:
	bool isJumpKeyDown = false;
	bool isRollKeyDown = false;
	bool isAttackKeyDown = false;
void processInput(const ExMessage& msg) {
	switch (msg.message) {
	case WM_KEYDOWN:
		switch (msg.vkcode) {
		case 0x41://A
			isLeftKeyDown = true;
			break;
		case 0x44://D
			isRightKeyDown = true;
			break;
		case 0x57://W
			isJumpKeyDown = true;
			break;
		case 0x53://S
			isRollKeyDown = true;
			break;
		default:
			break;
		}
		break;
	case WM_LBUTTONDOWN://鼠标左键
		isAttackKeyDown = true;
		break;
	case WM_KEYUP:
		switch (msg.vkcode) {
		case 0x41:
			isLeftKeyDown = false;
			break;
		case 0x44:
			isRightKeyDown = false;
			break;
		case 0x57:
			isJumpKeyDown = false;
			break;
		case 0x53:
			isRollKeyDown = false;
			break;
		default:
			break;
		}
		break;
	case WM_LBUTTONUP:
		isAttackKeyDown = false;
		break;
	default:
		break;
	}
}

跳跃

有了奔跑示例,跳跃也很简单,同样的,仅需在跳跃时赋予跳跃速度即可。

private:
	const float speedJump = 780;
//jump
if (isJumpKeyDown)
	zeroVelocity.y = -speedJump;

运行,发现依旧有bug,这是因为没有重力,而我们仅需要模拟重力即可。
模拟重力同样仅需一行代码。

private:
	const float G = 980 * 2;
zeroVelocity.y += G * deltaT;

运行发现小球直接掉下去,这是因为没有平台检测。我们在坐标更新后 添加 平台检测

private:
	const float floor = 340;
//平台检测
if (zeroCoordinate.y >= floor) {
	zeroCoordinate.y = floor;
	zeroVelocity.y = 0;
}

运行发现依旧有bug,这与跳跃的检测条件有关。而解决依旧很简单,添加一个条件即可。

if (isJumpKeyDown && isOnFloor())
	zeroVelocity.y = -speedJump;
private:
	bool isOnFloor() {
		return zeroCoordinate.y == floor;
	}

翻滚

攻击

不重要头文件

vector2.h

#pragma once
#include <cmath>

//自定义的加减乘除二维类

class Vector2{

public:
	float x = 0;
	float y = 0;

public:
	Vector2() = default;
	~Vector2() = default;

	Vector2(float x, float y)
		: x(x), y(y) {
	}

	Vector2 operator+(const Vector2& vec) const
	{
		return Vector2(x + vec.x, y + vec.y);
	}

	void operator+=(const Vector2& vec)
	{
		x += vec.x, y += vec.y;
	}

	void operator-=(const Vector2& vec)
	{
		x -= vec.x, y -= vec.y;
	}

	Vector2 operator-(const Vector2& vec) const
	{
		return Vector2(x - vec.x, y - vec.y);
	}

	float operator*(const Vector2& vec) const
	{
		return x * vec.x + y * vec.y;
	}

	Vector2 operator*(float val) const
	{
		return Vector2(x * val, y * val);
	}

	void operator*=(float val)
	{
		x *= val, y *= val;
	}

	float length()
	{
		return sqrt(x * x + y * y);
	}

	Vector2 normalize()
	{
		float len = length();

		if (len == 0)
			return Vector2(0, 0);

		return Vector2(x / len, y / len);
	}
};


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

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

相关文章

ros2笔记-2.5.3 多线程与回调函数

本节体验下多线程。 python示例 在src/demo_python_pkg/demo_python_pkg/下新建文件&#xff0c;learn_thread.py import threading import requestsclass Download:def download(self,url,callback):print(f线程&#xff1a;{threading.get_ident()} 开始下载&#xff1a;{…

C语言练习:求数组的最大值与最小值

文章目录 1. 提出任务2. 完成任务2.1 方法一&#xff1a;通过返回结构体指针来间接返回结果2.1.1 编写程序&#xff0c;实现功能2.1.2 运行程序&#xff0c;查看结果 2.2 方法二&#xff1a;通过参数传递数组&#xff0c;并在函数中修改传入的参数2.2.1 编写程序&#xff0c;实…

conda安装及demo:SadTalker实现图片+音频生成高质量视频

1.安装conda 下载各个版本地址&#xff1a;https://repo.anaconda.com/archive/ win10版本&#xff1a; Anaconda3-2023.03-1-Windows-x86_64 linux版本&#xff1a; Anaconda3-2023.03-1-Linux-x86_64 Windows安装 环境变量 conda -V2.配置conda镜像源 安装pip conda…

【前端系列01】优化axios响应拦截器

文章目录 一、前言&#x1f680;&#x1f680;&#x1f680;二、axios响应拦截器&#xff1a;☀️☀️☀️2.1 为什么前端需要响应拦截器element ui的消息组件 一、前言&#x1f680;&#x1f680;&#x1f680; ☀️ 回报不在行动之后&#xff0c;回报在行动之中。 这个系列可…

PingCAP 连续两年入选 Gartner 云数据库管理系统魔力象限“荣誉提及”

近日&#xff0c;全球 IT 市场研究和咨询公司 Gartner 发布最新报告《Magic Quadrant™ for Cloud Database Management Systems》&#xff08;云数据库管理系统魔力象限&#xff09;&#xff0c;PingCAP 因其企业级开源分布式数据库 TiDB 在全球市场的表现&#xff0c;连续两年…

CSS——2.书写格式一

<!DOCTYPE html> <html><head><meta charset"UTF-8"><title></title></head><body><!--css书写中&#xff1a;--><!--1.css 由属性名:属性值构成--><!--style"color: red;font-size: 20px;&quo…

QT上实现SVM进行数据分类

针对不了解SVM的原理的同学强推下面这个课程&#xff1a; 6.机器学习课程&#xff08;六&#xff09;支持向量机&#xff08;线性模型&#xff09;问题_哔哩哔哩_bilibili 一、QT实现SVM的方法 1.调用SVM的C语言库&#xff1a;麻烦&#xff0c;要专门去找库&#xff0c;cmak…

idea( 2022.3.2)打包报错总结

一 报错 class lombok.javac.apt.LombokProcessor (in unnamed module 0x4fe64d23) cannot access class com.sun.tools.javac.processing.JavacProcessingEnvironment (in module jdk.compiler) because module jdk.compiler does not export com.sun.tools.javac.processing …

极客说|微软 Phi 系列小模型和多模态小模型

作者&#xff1a;胡平 - 微软云人工智能高级专家 「极客说」 是一档专注 AI 时代开发者分享的专栏&#xff0c;我们邀请来自微软以及技术社区专家&#xff0c;带来最前沿的技术干货与实践经验。在这里&#xff0c;您将看到深度教程、最佳实践和创新解决方案。关注「极客说」&am…

解决 IntelliJ IDEA 中 Tomcat 日志乱码问题的详细指南

目录 前言1. 分析问题原因2. 解决方案 2.1 修改 IntelliJ IDEA 的 JVM 选项2.2 配置 Tomcat 实例的 VM 选项 2.2.1 设置 Tomcat 的 VM 选项2.2.2 添加环境变量 3. 进一步优化 3.1 修改 Tomcat 的 logging.properties3.2 修改操作系统默认编码 3.2.1 Windows 系统3.2.2 Linux …

某小程序sign签名参数逆向分析

文章目录 1. 写在前面2. 接口分析3. 分析还原 【&#x1f3e0;作者主页】&#xff1a;吴秋霖 【&#x1f4bc;作者介绍】&#xff1a;擅长爬虫与JS加密逆向分析&#xff01;Python领域优质创作者、CSDN博客专家、阿里云博客专家、华为云享专家。一路走来长期坚守并致力于Python…

医学图像分析工具02:3D Slicer || 医学影像可视化与分析工具 支持第三方插件

3D Slicer 是一款功能全面的开源医学影像分析软件&#xff0c;广泛应用于影像处理、三维建模、影像配准和手术规划等领域。它支持多种医学影像格式&#xff08;如 DICOM、NIfTI&#xff09;和丰富的插件扩展&#xff0c;是神经科学、放射学和生物医学研究中不可或缺的工具。 在…

Linux系统安装es详细教程

一、下载es及插件 从下面的网址进行对应es版本的下载https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-7.15.2-linux-x86_64.tar.gz &#xff0c;想要不同版本的es只需更换对应的版本号即可。 插件下载地址&#xff08;ik分词器、pinyin等&#xff09;es…

电子电气架构 --- 汽车总线基础介绍

我是穿拖鞋的汉子&#xff0c;魔都中坚持长期主义的汽车电子工程师。 老规矩&#xff0c;分享一段喜欢的文字&#xff0c;避免自己成为高知识低文化的工程师&#xff1a; 简单&#xff0c;单纯&#xff0c;喜欢独处&#xff0c;独来独往&#xff0c;不易合同频过着接地气的生活…

安装PyQt5-tools卡在Preparing metadata (pyproject.toml)解决办法

为了在VS code中使用PyQt&#xff0c;在安装PyQt5-tools时总卡在如下这一步 pyqt5 Preparing metadata (pyproject.toml)经过各种尝试&#xff0c;最终问题解决&#xff0c;在此记录方法。 首先进入PyQt5-tools官网查看其适配的Python版本&#xff0c;网址如下&#xff1a; h…

38 Opencv HOG特征检测

文章目录 HOGDescriptor 构造函数setSVMDetector 设置支持向量机&#xff08;SVM&#xff09;检测器&#xff0c;用于目标检测。compute 用于计算图像区域的HOG描述符。detectMultiScale 多尺度检测目标。示例 HOGDescriptor 构造函数 HOGDescriptor(); HOGDescriptor(const S…

分布式搜索引擎之elasticsearch基本使用3

分布式搜索引擎之elasticsearch基本使用3 1.部署单点es 1.1.创建网络 因为我们还需要部署kibana容器&#xff0c;因此需要让es和kibana容器互联。这里先创建一个网络&#xff1a; docker network create es-net1.2.加载镜像 这里我们采用elasticsearch的7.12.1版本的镜像&…

人工智能知识分享第九天-机器学习_集成学习

集成学习 概念 集成学习是机器学习中的一种思想&#xff0c;它通过多个模型的组合形成一个精度更高的模型&#xff0c;参与组合的模型称为弱学习器&#xff08;基学习器&#xff09;。训练时&#xff0c;使用训练集依次训练出这些弱学习器&#xff0c;对未知的样本进行预测时…

在线机考|2024华为实习秋招春招编程题(最新)——第3题_个性化歌单推荐系统_300分(十一)

题目内容 假设你是音乐服务的开发者,为了提高用户体验需要解决推荐歌单的同质化问题,保证推荐给用户的所有歌单不包含相同歌曲的。给定一个包含N个歌单和M条歌单重复记录,每个歌单用一个从1到N的整数编号,歌单重复记录包含两个歌单的ID,表示两个歌单有相同的歌曲。 你的任…

学英语学压测:02jmeter组件-测试计划和线程组ramp-up参数的作用

&#x1f4e2;&#x1f4e2;&#x1f4e2;&#xff1a;先看关键单词&#xff0c;再看英文&#xff0c;最后看中文总结&#xff0c;再回头看一遍英文原文&#xff0c;效果更佳&#xff01;&#xff01; 关键词 Functional Testing功能测试[ˈfʌŋkʃənəl ˈtɛstɪŋ]Sample样…