Hazel -- Game Engine Series (二)

A simple game engine based of The Cherno’s Youtube Tutorial Series.

Entry Point

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

Log

在这里插入图片描述

//log.h
#pragma once

#include <memory>
#include "spdlog/spdlog.h"
#include "spdlog/sinks/stdout_color_sinks.h"
#include "Core.h"

namespace Hazel {

	class HAZEL_API Log
	{
	public:
		Log() {}
		~Log() {}

		static void Init();
		inline static std::shared_ptr<spdlog::logger>& GetCoreLogger() { return m_CoreLogger; }
		inline static std::shared_ptr<spdlog::logger>& GetClienteLogger() { return m_ClientLogger; }
	private:
		static std::shared_ptr<spdlog::logger> m_CoreLogger;
		static std::shared_ptr<spdlog::logger> m_ClientLogger;
	};

}

#define HZ_CORE_TRACE(...)		::Hazel::Log::GetCoreLogger()->trace(__VA_ARGS__)
#define HZ_CORE_INFO(...)		::Hazel::Log::GetCoreLogger()->info(__VA_ARGS__)
#define HZ_CORE_WARN(...)		::Hazel::Log::GetCoreLogger()->warn(__VA_ARGS__)
#define HZ_CORE_ERROR(...)		::Hazel::Log::GetCoreLogger()->error(__VA_ARGS__)
#define HZ_CORE_FATAL(...)		::Hazel::Log::GetCoreLogger()->fatal(__VA_ARGS__)

#define HZ_TRACE(...)			::Hazel::Log::GetClienteLogger()->trace(__VA_ARGS__)
#define HZ_INFO(...)			::Hazel::Log::GetClienteLogger()->info(__VA_ARGS__)
#define HZ_WARN(...)			::Hazel::Log::GetClienteLogger()->warn(__VA_ARGS__)
#define HZ_ERROR(...)			::Hazel::Log::GetClienteLogger()->error(__VA_ARGS__)
#define HZ_FATAL(...)			::Hazel::Log::GetClienteLogger()->fatal(__VA_ARGS__)
//log.cpp
#include "Log.h"

namespace Hazel {

	std::shared_ptr<spdlog::logger> Log::m_CoreLogger;
	std::shared_ptr<spdlog::logger> Log::m_ClientLogger;

	void Log::Init()
	{
		spdlog::set_pattern("%^[%T] %n: %v%$");
		m_CoreLogger = spdlog::stdout_color_mt("HAZEL");
		m_CoreLogger->set_level(spdlog::level::trace);

		m_ClientLogger = spdlog::stdout_color_mt("APP");
		m_ClientLogger->set_level(spdlog::level::trace);
	}
}

premake

在这里插入图片描述
在这里插入图片描述

-- premake5.lua

workspace "Hazel"
    architecture "x64"

    configurations 
    { "Debug", "Release", "Dist" }

outputdir = "%{cfg.buildcfg}-%{cfg.system}-%{cfg.architecture}"

project "Hazel"
     location "Hazel"
     kind "SharedLib"
     language "C++"

     targetdir ("bin/" .. outputdir .. "/%{prj.name}")
     objdir ("bin-int/" .. outputdir .. "/%{prj.name}")

     files 
     { 
        "%{prj.name}/src/**.h",
        "%{prj.name}/src/**.cpp" 
     }

     includedirs
     {
          "%{prj.name}/vendor/spdlog/include"
     }
   
     filter "system:windows"
          cppdialect "C++17"
          staticruntime "On"
          systemversion "10.0.19041.0"
          
          defines
          {
               "HZ_PLATFORM_WINDOWS",
               "HZ_BUILD_DLL"
          }

          postbuildcommands
          {
               ("{COPY} %{cfg.buildtarget.relpath} ../bin/" .. outputdir .. "/Sandbox")
          }

     filter "configurations:Debug"
          defines "HZ_DEBUG"
          symbols "On"

     filter "configurations:Release"
          defines "HZ_RELEASE"
          optimize "On"

     filter "configurations:Dist"
          defines "HZ_DIST"
          optimize "On"

project "Sandbox"
     location "Sandbox"
     kind "ConsoleApp"
     language "C++"

     targetdir ("bin/" .. outputdir .. "/%{prj.name}")
     objdir ("bin-int/" .. outputdir .. "/%{prj.name}")

     files 
     { 
          "%{prj.name}/src/**.h",
          "%{prj.name}/src/**.cpp" 
     }

     includedirs
     {
          "Hazel/vendor/spdlog/include",
          "Hazel/src"
     }

     links
     {
          "Hazel"
     }
     
     filter "system:windows"
          cppdialect "C++17"
          staticruntime "On"
          systemversion "10.0.19041.0"
          
          defines
          {
               "HZ_PLATFORM_WINDOWS"
          }

     filter "configurations:Debug"
          defines "HZ_DEBUG"
          symbols "On"

     filter "configurations:Release"
          defines "HZ_RELEASE"
          optimize "On"

     filter "configurations:Dist"
          defines "HZ_DIST"
          optimize "On"

precompile headers

在这里插入图片描述
修改premake5.lua
在这里插入图片描述

Event

//Event.h
#pragma once

#include "hzpch.h"
#include "Hazel/Core.h"

#define BIT(x) 1 << x

namespace Hazel {

	// 事件的详细类型
	enum class HAZEL_API EventType
	{
		None = 0,
		WindowClose, WindowResize, WindowFocus, WindowLostFocus, WindowMoved,
		AppTick, AppUpdate, AppRender,
		KeyPressed, KeyReleased,
		MouseButtonPressed, MouseButtonReleased, MouseMoved, MouseScrolled
	};

	// 事件的大体种类
	enum EventCategory
	{
		None = 0,
		EventCategoryApplication = BIT(0),
		EventCategoryInput		 = BIT(1),
		EventCategoryKeyboard	 = BIT(2),
		EventCategoryMouse		 = BIT(3),
		EventCategoryMouseButton = BIT(4),
	};

#define EVENT_CLASS_TYPE(type) \
			static EventType GetStaticType() { return EventType::##type; }\
			const EventType GetEventType() const override { return GetStaticType(); }\
			const char* GetName() const override { return #type; }

#define EVENT_CLASS_CATEGORY(category) \
			virtual int GetCategoryFlag() const override { return category; }

	class HAZEL_API Event
	{
	public:
		virtual const char* GetName() const = 0;
		virtual const EventType GetEventType() const = 0;
		virtual int GetCategoryFlag() const = 0;
		virtual std::string ToString() const { return GetName(); }
		inline bool IsInCategory(EventCategory type)
		{
			return GetCategoryFlag() & type;
		}

	protected:
		bool m_Handled = false;//用来标记这个事件是否被处理了
	};

	class EventDistpatcher
	{
		template<typename T>
		using Evenfn = std::function<bool(T&)>;
	public:
		EventDistpatcher(Event& event)
			:m_Event(event) {}

		template<typename T>
		bool Dispatch(Evenfn<T> func) 
		{
			if (m_Event.GetEventType == T::GetStaticType) 
			{
				m_Event.m_Handled = func(*(T*)&m_Event);
			}
			return false;
		}

	private:
		Event& m_Event;
	};
}

ApplicationEvent

//ApplicationEvent.h
#pragma once
#include "Event.h"

// Windows和APP相关的Event都在这里处理
namespace Hazel
{
	class HAZEL_API WindowCloseEvent :public Event
	{
	public:
		WindowCloseEvent() {}
		EVENT_CLASS_TYPE(WindowClose)
		EVENT_CLASS_CATEGORY(EventCategoryApplication)

		std::string ToString() const override
		{
			std::stringstream a;
			a << "Window Close";
			return a.str();
		}
	protected:
	};

	class HAZEL_API  WindowResizedEvent :public Event
	{
	public:
		WindowResizedEvent(int height, int width) :m_Height(height), m_Width(width) {}
		EVENT_CLASS_TYPE(WindowResize)
		EVENT_CLASS_CATEGORY(EventCategoryApplication)

		inline int GetWindowHeight() { return m_Height; }
		inline int GetWindowWidth() { return m_Width; }
		std::string ToString() const override
		{
			std::stringstream a;
			a << "Window Resize: width = " << m_Width << ", height = " << m_Height;
			return a.str();
		}

	protected:
		int m_Height, m_Width;
	};

	class HAZEL_API AppTickEvent : public Event
	{
	public:
		AppTickEvent() {}

		EVENT_CLASS_TYPE(AppTick)
		EVENT_CLASS_CATEGORY(EventCategoryApplication)
	};

	class HAZEL_API AppUpdateEvent : public Event
	{
	public:
		AppUpdateEvent() {}

		EVENT_CLASS_TYPE(AppUpdate)
		EVENT_CLASS_CATEGORY(EventCategoryApplication)
	};

	class HAZEL_API AppRenderEvent : public Event
	{
	public:
		AppRenderEvent() {}

		EVENT_CLASS_TYPE(AppRender)
		EVENT_CLASS_CATEGORY(EventCategoryApplication)
	};
}

KeyEvent

//KeyEvent.h
#pragma once

#include "Event.h"

namespace Hazel {
	class HAZEL_API KeyEvent : public Event
	{
	public:
		EVENT_CLASS_CATEGORY(EventCategoryKeyboard | EventCategoryInput)
		inline int GetKeycode() const { return m_KeyCode; }

	protected:
		// 构造函数设为Protected,意味着只有其派生类能够调用此函数
		KeyEvent(int code) : m_KeyCode(code) {}
		int m_KeyCode;
	};

	class HAZEL_API KeyPressedEvent : public KeyEvent
	{
	public:
		KeyPressedEvent(int keycode, int repeatCount)
			: KeyEvent(keycode), m_RepeatCount(repeatCount) {}
		inline int GetRepeatCount() const { return m_RepeatCount; }
		std::string ToString() const override
		{
			std::stringstream ss;
			ss << "KeyPressedEvent: " << m_KeyCode << " (" << m_RepeatCount << " repeats)";
			return ss.str();
		}
		EVENT_CLASS_TYPE(KeyPressed)
	private:
		int m_RepeatCount;
	};

	class HAZEL_API KeyReleasedEvent : public KeyEvent
	{
	public:
		KeyReleasedEvent(int keycode)
			: KeyEvent(keycode) {}

		std::string ToString() const override
		{
			std::stringstream ss;
			ss << "KeyReleasedEvent: " << m_KeyCode;
			return ss.str();
		}

		EVENT_CLASS_TYPE(KeyReleased)
	};
}

MouseEvent

//MouseEvent.h
#pragma once

#include "Event.h"

namespace Hazel {
	class MouseMovedEvent : public Event
	{
	public:
		static EventType GetStaticType() { return EventType::MouseMoved; }
		const EventType GetEventType() const override { return GetStaticType(); }
		const char* GetName() const override { return "MouseMoved"; }
		std::string ToString()
		{
			// Create a string with represents 
			std::string a = "MouseMovedEvent: xOffset = " + GetXOffset() + ", yOffset = " + GetYOffset();
			return a;
		}

		inline float GetXOffset() const { return m_xOffset; }
		inline float GetYOffset() const { return m_yOffset; }
	private:
		float m_xOffset, m_yOffset;
	};
}

GLFW

在这里插入图片描述
然后GLFW文件夹添加一个premake5.lua文件

Window.h

#include "hzpch.h"

#include "Event/Event.h"
#include "Core.h"

namespace Hazel {

	struct HAZEL_API WindowProps {
	public:
		std::string m_title;
		unsigned int m_width;
		unsigned int m_height;
		WindowProps(const std::string& title = "Hazel Engine", unsigned int width = 1280, unsigned int height = 720)
			:m_title(title), m_width(width), m_height(height) {

		}
	};

	class HAZEL_API Window
	{
	public:
		using EventCallbackFn = std::function<void(Event&)>;
		virtual ~Window() = default;
		virtual int GetWindowWidth() const = 0;
		virtual int GetWindowHeight() const = 0;
		virtual bool IsVSync() const = 0;
		virtual void SetVSync(bool) = 0;
		virtual void OnUpdate() = 0;
		virtual void OnResized(unsigned int width, unsigned int height) = 0;
		//virtual void* GetNaiveWindow() = 0;
		virtual void SetEventCallback(const EventCallbackFn& callback) = 0;

		static Window* Create(const WindowProps& props = WindowProps());
	};
}

WindowsWindow.h

#pragma once

#include "Hazel/Window.h"
#include <GLFW/glfw3.h>

namespace Hazel {

	class WindowsWindow : public Window
	{
	public:
		WindowsWindow(const WindowProps& props);
		~WindowsWindow() override;
		int GetWindowWidth() const  override { return m_Data.width; }
		int GetWindowHeight() const override { return m_Data.height; }
		bool IsVSync() const override { return m_Data.isVSync; }
		void SetVSync(bool enabled) override;
		void OnUpdate() override;
		void OnResized(unsigned int width, unsigned int height) override;
		//void* GetNaiveWindow() override;
		void SetEventCallback(const EventCallbackFn& callback) override { m_Data.eventCallback = callback; };

	private:
		virtual void Shutdown();
		virtual void Init(const WindowProps& props);

	private:
		GLFWwindow* m_Window;

		// WindowData与WindowProp不同,这个创建这个类是为了方便和glfw交互
		struct WindowData
		{
			std::string title;
			unsigned int width, height;
			bool isVSync;
			EventCallbackFn eventCallback;
		};
		WindowData m_Data;
	};

}

WindowsWindow.cpp

#include "hzpch.h"
#include "WindowsWindow.h"

namespace Hazel 
{
	static bool m_initialized = false;

	Window* Window::Create(const WindowProps& props)
	{
		return new WindowsWindow(props);
	}

	WindowsWindow::WindowsWindow(const WindowProps& props)
	{
		Init(props);
	}

	WindowsWindow::~WindowsWindow()
	{
		Shutdown();
	}

	void WindowsWindow::Init(const WindowProps& props)
	{
		if (!m_initialized)
		{
			m_initialized = true;
			int r = glfwInit();
			//HAZEL_ASSERT(r, "Failed to init glfw!")
		}

		m_Data.title = props.m_title;
		m_Data.width = props.m_width;
		m_Data.height = props.m_height;

		//HZ_CORE_INFO("Creating Window {0} {1} {2}", props.m_title, props.m_width, props.m_height);
		
		m_Window = glfwCreateWindow(m_Data.width, m_Data.height, m_Data.title.c_str(), NULL, NULL);
		glfwMakeContextCurrent(m_Window);
		glfwSetWindowUserPointer(m_Window, &m_Data);
		SetVSync(true);

	}

	void WindowsWindow::Shutdown()
	{
		glfwDestroyWindow(m_Window);
	}

	void WindowsWindow::SetVSync(bool enabled)
	{
		if (enabled)
			glfwSwapInterval(1);
		else
			glfwSwapInterval(0);

		m_Data.isVSync = enabled;
	}

	void WindowsWindow::OnUpdate()
	{
		glfwPollEvents();
		glfwSwapBuffers(m_Window);
	}

	void WindowsWindow::OnResized(unsigned int width, unsigned int height)
	{
		return;
	}

	/*void* WindowsWindow::GetNaiveWindow()
	{
		return;
	}*/
}

Application.cpp

Application::Application()
	{
		m_Window = std::unique_ptr<Window>(Window::Create());
	}
	void Application::run()
	{
		/*WindowResizedEvent we(1200, 700);
		HZ_TRACE(we.ToString());*/

		while (m_Running)
		{
			m_Window->OnUpdate();
		}
	}

测试效果:
在这里插入图片描述

基于GLFW库,创建Hazel引擎的Window类和Event类

Event类

抽象Event类接口

	class HAZEL_API Event
	{
	public:
		virtual const char* GetName() const = 0;
		virtual const EventType GetEventType() const = 0;
		virtual int GetCategoryFlag() const = 0;
		virtual std::string ToString() const { return GetName(); }
		inline bool IsInCategory(EventCategory type)
		{
			return GetCategoryFlag() & type;
		}
	protected:
		bool m_Handled = false;//用来标记这个事件是否被处理了
	};

具体事件的实现,例如实现按键盘的事件:

	class HAZEL_API KeyPressedEvent : public Event
	{
	public:
		KeyPressedEvent(int keycode, int keyRepeated)
			:m_Keycode(keycode), m_KeyRepeated(keyRepeated) {}
		inline int GetRepeated() const { return m_KeyRepeated; }

		std::string ToString()const override
		{
			std::stringstream ss;
			ss << "KeyPressedEvent:\n KeyCode : " << m_Keycode << " KeyRepeated: " << m_KeyRepeated;
			return ss.str();
		}
		static EventType GetStaticType() {return EventType::KeyPressed; } // 此类下的Event类型都是一样的,所以应该设为Static
		virtual EventType GetEventType() const override {return GetStaticType();} // 写这个是为了防止没有KeyEvent类型,只有Event类型
		virtual const char* GetName() const override { return "KeyPressed"; } 
	protected:
		int m_KeyRepeated;
		int m_Keycode;
	};

Window类

Window类作为接口类,需要包含通用的窗口内容:

  • List item
  • 虚析构函数
  • 一个Update函数,用于在loop里每帧循环
  • 窗口的长和宽,以及相应的Get函数
  • 设置窗口的Vsync和Get窗口的Vsync函数
  • 窗口的回调函数,当窗口接受事件输入时,会调用这个回调函数

所以Windows接口类设计如下:

class HAZEL_API Window
	{
	public:
		// Window自带一个回调函数,用来处理从glfw库收到的callback
		using EventCallbackFn = std::function<void(Event&)>;
		virtual ~Window() {};
		virtual float const& GetWindowHeight() const = 0;
		virtual float const& GetWindowWidth() const = 0;
		virtual bool IsVSync() const = 0;
		virtual void SetVSync(bool) = 0;
		virtual void OnUpdate() = 0;
		virtual void SetEventCallback(const EventCallbackFn& callback) = 0;

		static Window* Create(const WindowProps& props = WindowProps());
	};

由Application创建window,同时Application给与window对应的回调函数,让window接受glfw的回调函数后,再来调用对应Application的回调函数,而window本身是不知道Application的存在的,设计代码如下:

Application::Application()
	{
		m_Window = std::unique_ptr<Window>(Window::Create());
		m_Window->SetEventCallback(std::bind(&Application::OnEvent, this, std::placeholders::_1));// 设置window的callback为此对象的OnEvent函数
		// 像下面这样直接写lambda也是可以的
		//m_Window->SetEventCallback([](Event& e)->void
		//{
		//	if (e.GetEventType() == EventType::MouseScrolled)
		//	{
		//		MouseScrolledEvent ee = (MouseScrolledEvent&)e;
		//		LOG( "xOffset:{0} and yOffset:{1}", ee.GetXOffset(), ee.GetYOffset());
		//	}
		//}
		//);
	}
	
	void Application::OnEvent(Event& e)
	{
		//CORE_LOG("{0}", e);
		CORE_LOG(e.ToString());
		EventDispatcher dispatcher(e);
		dispatcher.Dispatch<WindowCloseEvent>(std::bind(&Application::OnWindowClose, this, std::placeholders::_1));
	}
	
	void Application::Run() 
	{
		std::cout << "Run Application" << std::endl;
		while (m_Running)
		{
			// Application并不应该知道调用的是哪个平台的window,Window的init操作放在Window::Create里面
			// 所以创建完window后,可以直接调用其loop开始渲染
			m_Window->OnUpdate();
		}
		//LOG(w.ToString());
	}

	bool Application::OnWindowClose(WindowCloseEvent &e)
	{
		m_Running = false;
		return true;
	}

其中,EventDispatcher用于根据事件类型的不同,调用不同的函数:

// 当收到Event时,创建对应的EventDispatcher
	class HAZEL_API EventDispatcher 
	{
		template<typename T>
		using EventHandler = std::function<bool(T&)>;//EventHandler存储了一个输入为任意类型的引用,返回值为bool的函数指针
	public:
		EventDispatcher(Event& event):
			m_Event(event){}

		// T指的是事件类型, 如果输入的类型没有GetStaticType会报错
		template<typename T>
		void Dispatch(EventHandler<T> handler)
		{
			if (m_Event.m_Handled)
				return;

			if (m_Event.GetEventType() == T::GetStaticType()) 
			{
				m_Event.m_Handled = handler(*(T*)&m_Event); //使用(T*)把m_Event转换成输入事件的指针类型
			}
		}

	private:
		Event& m_Event;//必须是引用,不可以是Event的实例,因为Event带有纯虚函数
	};

最终运行结果如下,在console窗口上打印GLFW窗口触发的事件信息
在这里插入图片描述

附录

git submodule 删除子模块

网上流传了一些偏法,主要步骤是直接移除模块,并手动修改 .gitmodules、.git/config 和 .git/modules 内容。包含了一大堆类似git rm --cached 、rm -rf 、rm .gitmodules 和 git rm --cached 之类的代码。

实际上这是一种比较野的做法,不建议使用。

根据官方文档的说明,应该使用 git submodule deinit 命令卸载一个子模块。这个命令如果添加上参数 --force,则子模块工作区内即使有本地的修改,也会被移除。

git submodule deinit project-sub-1

git rm project-sub-1

执行 git submodule deinit project-sub-1 命令的实际效果,是自动在 .git/config 中删除了以下内容:

[submodule “project-sub-1”]
url = https://github.com/username/project-sub-1.git
执行 git rm project-sub-1 的效果,是移除了 project-sub-1 文件夹,并自动在 .gitmodules 中删除了以下内容:

[submodule “project-sub-1”]
path = project-sub-1
url = https://github.com/username/project-sub-1.git
此时,主项目中关于子模块的信息基本已经删除(虽然貌似 .git/modules 目录下还有残余):

➜ project-main git:(master) ✗ gs
位于分支 master
您的分支与上游分支 ‘origin/master’ 一致。
要提交的变更:
(使用 “git reset HEAD <文件>…” 以取消暂存)
修改: .gitmodules
删除: project-sub-1
可以提交代码:

git commit -m “delete submodule project-sub-1”

至此完成对子模块的删除。

遇到的GLFW库使用问题

trouble linking with glfw using premake and vs2019 报错如下:

3>GLFW.lib(init.obj) : error LNK2019: unresolved external symbol
_glfwSelectPlatform referenced in function glfwInit 3>GLFW.lib(vulkan.obj) : error LNK2019: unresolved external symbol
_glfwPlatformLoadModule referenced in function _glfwInitVulkan 3>GLFW.lib(vulkan.obj) : error LNK2019: unresolved external symbol
_glfwPlatformFreeModule referenced in function _glfwInitVulkan 3>GLFW.lib(vulkan.obj) : error LNK2019: unresolved external symbol
_glfwPlatformGetModuleSymbol referenced in function _glfwInitVulkan

修改glfw的premake5.lua如下:

project "GLFW"
kind "StaticLib"
language "C"

targetdir ("bin/" .. outputdir .. "/%{prj.name}")
objdir ("bin-int/" .. outputdir .. "/%{prj.name}")

files
{
    "include/GLFW/glfw3.h",
    "include/GLFW/glfw3native.h",
    "src/internal.h",
    "src/platform.h",
    "src/mappings.h",
    "src/context.c",
    "src/init.c",
    "src/input.c",
    "src/monitor.c",
    "src/platform.c",
    "src/vulkan.c",
    "src/window.c",
    "src/egl_context.c",
    "src/osmesa_context.c",
    "src/null_platform.h",
    "src/null_joystick.h",
    "src/null_init.c",

    "src/null_monitor.c",
    "src/null_window.c",
    "src/null_joystick.c",

}
filter "system:linux"
    pic "On"

    systemversion "latest"
    staticruntime "On"

    files
    {
        "src/x11_init.c",
        "src/x11_monitor.c",
        "src/x11_window.c",
        "src/xkb_unicode.c",
        "src/posix_time.c",
        "src/posix_thread.c",
        "src/glx_context.c",
        "src/egl_context.c",
        "src/osmesa_context.c",
        "src/linux_joystick.c"
    }

    defines
    {
        "_GLFW_X11"
        
    }

filter "system:windows"
    systemversion "latest"
    staticruntime "On"
    
    -- buildoptions{
    --     "/MT"
    -- }

    files
    {
        "src/win32_init.c",
        "src/win32_module.c",
        "src/win32_joystick.c",
        "src/win32_monitor.c",
        "src/win32_time.h",
        "src/win32_time.c",
        "src/win32_thread.h",
        "src/win32_thread.c",
        "src/win32_window.c",
        "src/wgl_context.c",
        "src/egl_context.c",
        "src/osmesa_context.c"
    }

    defines 
    { 
        "_GLFW_WIN32",
        "_CRT_SECURE_NO_WARNINGS"

    }

filter "configurations:Debug"
    runtime "Debug"
    symbols "On"

filter "configurations:Release"
    runtime "Release"
    optimize "On"

glfw项目重新生成后即可使用

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值