目 录CONTENT

文章目录

【2D平台跳跃游戏 - 02】绘图

千年的霜雪
2024-09-17 / 0 评论 / 1 点赞 / 11 阅读 / 0 字 / 正在检测是否收录...

【2D平台跳跃游戏 - 02】绘图

摄像机类

摄像机作用

摄像机作为玩家观察游戏世界的工具:

  1. 当使用摄像机渲染世界的一部分,同时跟随玩家移动时,就可以做出“世界感”
  2. 摄像机可以用来制作一些特效,例如屏幕抖动
  3. 摄像机还可以进行一些画面的后处理

由于这次的游戏比较小,用不到摄像机跟随玩家移动的功能,所以此次只是用于实现一些特效

窗口坐标系与世界坐标系

在窗口中渲染时,实际上使用的坐标是物体相对于摄像机的坐标

整个游戏世界的所有运行逻辑,如玩家移动或碰撞检测等,都应该使用世界坐标系
只有在需要绘图时,才会转换为窗口坐标系绘制;
这也正是数据与渲染分离的思想

两种坐标的换算公式为:
窗口坐标 = 世界坐标 - 摄像机坐标

窗口抖动

窗口抖动其实就是令摄像机快速变换坐标,就可以实现窗口抖动

如果想要更加平滑,可以使用柏林噪声等算法优化抖动函数

这里使用的是最简单的方法,设置一个抖动半径,摄像机坐标在抖动半径的圆内随机取点,实现抖动效果

实际实现

实际实现时,使用了自己封装的二维向量类与定时器类

在摄像机类中,封装了:

  1. 构造函数,实现了初始化摄像机状态
  2. 获取摄像机位置,使用了const修饰
  3. 重置摄像机方法
  4. 摄像机更新方法
  5. 窗口抖动方法

私有成员中,存储了四个变量:

  1. 二维向量记录摄像机位置
  2. 摄像机抖动定时器
  3. 布尔变量记录摄像机是否正在抖动
  4. 浮点变量记录摄像机抖动强度
#pragma once

#include "vector2.h"
#include "timer.h"

// 摄像机类
class Camera
{
public:
	Camera()
	{
		timer_shake.set_one_shot(true);
		timer_shake.set_callback([&]()
			{
				is_shaking = false;
				reset();
			});
	}

	~Camera() = default;

	// 获取摄像机位置
	const Vector2& get_position() const
	{
		return position;
	} // 第一个const修饰返回值,设定返回值为常量引用

	// 重置摄像机方法
	void reset()
	{
		position.x = 0;
		position.y = 0;
	}

	// 摄像机更新方法
	void on_update(int delta)
	{
		timer_shake.on_update(delta);

		if (is_shaking) // 在单位圆内随机取坐标完成抖动
		{
			position.x = (-50 + rand() % 100) / 50.0f * shaking_strength;
			position.y = (-50 + rand() % 100) / 50.0f * shaking_strength;
		}
	}

	// 窗口抖动方法
	void shake(float strength, int duration)
	{
		is_shaking = true;
		shaking_strength = strength;

		timer_shake.set_wait_time(duration);
		timer_shake.restart();
	}

private:
	Vector2 position; // 摄像机位置
	Timer timer_shake; // 摄像机抖动定时器
	bool is_shaking = false; // 摄像机是否正在抖动
	float shaking_strength = 0; // 摄像机抖动幅度
};

图集类

图集类似于容器,用于批量加载和存储一套动画所用的素材

加载图片时,由于使用的素材命名都有规律,所以可以使用字符串拼接批量加载一类素材

#pragma once

#include <vector>
#include <graphics.h>

// 图集类
class Atlas
{
public:
	Atlas() = default;
	~Atlas() = default;

	// 加载图片
	void load_from_file(LPCTSTR path_template, int num)
	{
		img_list.clear();
		img_list.resize(num);

		TCHAR path_file[256];
		for (int i = 0; i < num; i++)
		{
			_stprintf_s(path_file, path_template, i + 1);
			loadimage(&img_list[i], path_file);
		}
	}

	// 清空图集
	void clear()
	{
		img_list.clear();
	}

	// 获取图集中图片数量
	int get_size()
	{
		return (int)img_list.size();
	}

	// 获取实际渲染的动画帧
	IMAGE* get_image(int idx)
	{
		if (idx < 0 || idx >= img_list.size())
			return nullptr;

		return &img_list[idx];
	}

	// 添加已有的图片
	void add_image(const IMAGE& img)
	{
		img_list.push_back(img);
	}

private:
	std::vector<IMAGE> img_list;
};

动画类

实现思路

动画类似于一个只记录当前播放状态的轻量管理器

在渲染时需要实时地去对应的图集中获取具体的图片

动画类中同样使用了摄像机类与定时器类

动画渲染

图片渲染函数被封装在util.h头文件中,支持使用摄像机和裁剪的重载

#include <graphics.h>
#include "camera.h"

#pragma comment(lib, "MSIMG32.LIB")

// 基础透明渲染
inline void putimage_alpha(int dst_x, int dst_y, IMAGE* img)
{
	int w = img->getwidth();
	int h = img->getheight();
	AlphaBlend(GetImageHDC(GetWorkingImage()), dst_x, dst_y, w, h,
		GetImageHDC(img), 0, 0, w, h, { AC_SRC_OVER, 0, 255, AC_SRC_ALPHA });
}

// 支持摄像机
inline void putimage_alpha(const Camera& camera, int dst_x, int dst_y, IMAGE* img)
{
	int w = img->getwidth();
	int h = img->getheight();
	const Vector2& pos_camera = camera.get_position();
	AlphaBlend(GetImageHDC(GetWorkingImage()), (int)(dst_x - pos_camera.x), (int)(dst_y - pos_camera.y), w, h,
		GetImageHDC(img), 0, 0, w, h, { AC_SRC_OVER, 0, 255, AC_SRC_ALPHA });
}

// 支持裁剪
inline void putimage_alpha(int dst_x, int dst_y, int width, int height, IMAGE* img, int src_x, int src_y)
{
	int w = width > 0 ? width : img->getwidth();
	int h = height > 0 ? height : img->getheight();
	AlphaBlend(GetImageHDC(GetWorkingImage()), dst_x, dst_y, w, h,
		GetImageHDC(img), src_x, src_y, w, h, { AC_SRC_OVER, 0, 255, AC_SRC_ALPHA });
}

具体实现

动画类支持了回调函数,方便实现在动画播放结束后执行某些逻辑,如销毁子弹等
回调函数使用C++的模板库中的functional封装

#pragma once

#include <graphics.h>
#include <functional>
#include "atlas.h"
#include "util.h"
#include "camera.h"

// 动画类
class Animation
{
public:
	Animation() = default;
	~Animation() = default;

	// 重置动画状态
	void reset()
	{
		timer = 0;
		idx_frame = 0;
	}

	// 设置对应的图集
	void set_atlas(Atlas* new_atlas)
	{
		reset();
		atlas = new_atlas;
	}
	
	// 设置是否循环播放
	void set_loop(bool flag)
	{
		is_loop = flag;
	}

	// 设置帧间隔
	void set_interval(int ms)
	{
		interval = ms;
	}

	// 获取当前帧需要渲染的图片编号
	int get_idx_frame()
	{
		return idx_frame;
	}

	// 获取当前帧需要渲染的图片
	IMAGE* get_frame()
	{
		return atlas->get_image(idx_frame);
	}

	//  检查是否播放结束
	bool check_finished()
	{
		if (is_loop) return false;

		return (idx_frame == atlas->get_size() - 1);
	}

	// 更新方法
	void on_update(int delta)
	{
		timer += delta;
		if (timer >= interval)
		{
			timer = 0;
			idx_frame++;
			if (idx_frame >= atlas->get_size())
			{
				idx_frame = is_loop ? 0 : atlas->get_size() - 1;
				if (!is_loop && callback) // 如果动画没有设置循环且回调函数存在
				{
					callback();
				}
			}
		}
	}

	// 绘制方法
	void on_draw(const Camera& camera,int x, int y) const
	{
		putimage_alpha(camera, x, y, atlas->get_image(idx_frame));
	}

	// 设置回调函数
	void set_callback(std::function<void()> callback)
	{
		this->callback = callback;
	}

private:
	int timer = 0; // 计时器
	int interval = 0; // 帧间隔
	int idx_frame = 0; // 帧索引
	bool is_loop = true; // 是否循环
	Atlas* atlas = nullptr;
	std::function<void()> callback;
};
1

评论区