当前位置: 首页 > news >正文

easyx空洞武士项目总结


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


文章目录

  • 前言
  • 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
//更新后为t1lastUpdateTime = 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) {//runif (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;
elsezeroVelocity.x = 0;

完整代码如下

void update(float deltaT) {//runrunDirection = isRightKeyDown - isLeftKeyDown;if (runDirection)zeroVelocity.x = runDirection * speedRun;elsezeroVelocity.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://AisLeftKeyDown = true;break;case 0x44://DisRightKeyDown = true;break;case 0x57://WisJumpKeyDown = true;break;case 0x53://SisRollKeyDown = 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.mrgr.cn/news/82750.html

相关文章:

  • EXCEL技巧
  • NLP项目实战——基于Bert模型的多情感评论分类(附数据集和源码)
  • requests中data和json的区别
  • 【C++】:浅析 “std::cout << std::endl”
  • MySQL insert or update方式性能比较
  • 5.2 数据库:UPDATE子句
  • 基于springboot+vue的餐饮连锁店管理系统的设计与实现
  • 3272 小蓝的漆房
  • 【设计模式-2】23 种设计模式的分类和功能
  • 代码随想录算法训练营day23
  • 安装Linux
  • Oracle Dataguard(主库为单节点)配置详解(3):配置备库
  • 力扣【SQL连续问题】
  • App窗口创建流程(Android12 )
  • (已开源-AAAI25) RCTrans:雷达相机融合3D目标检测模型
  • 【设计模式-1】软件设计模式概述
  • 【Python】绿色便携版Python制作(发布)方法
  • Oracle Dataguard(主库为单节点)配置详解(2):配置主库
  • 智能工厂的设计软件 应用场景的一个例子: 为AI聊天工具添加一个知识系统 之21 再次重建 之6 整“拼” 项目文档+程序框架 总述
  • 【Linux】函数
  • 1.2.1-2部分数据结构的说明02_链表
  • 简历_专业技能_熟悉分布式锁Redisson的原理以及使用
  • QML学习(七) 学习QML时,用好Qt设计器,快速了解各个组件的属性
  • 使用Dinky快速提交Flink operator任务
  • Elasticsearch 入门教程
  • Unity2D初级背包设计中篇 MVC分层撰写(万字详解)