diff --git a/.clang-format b/.clang-format new file mode 100644 index 00000000..03ada50f --- /dev/null +++ b/.clang-format @@ -0,0 +1,97 @@ +--- +Language: Cpp +BasedOnStyle: Google +AccessModifierOffset: -1 +AlignAfterOpenBracket: Align +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +AlignEscapedNewlinesLeft: true +AlignOperands: true +AlignTrailingComments: true +AllowAllParametersOfDeclarationOnNextLine: false +AllowShortBlocksOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: false +AllowShortIfStatementsOnASingleLine: false +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: true +AlwaysBreakTemplateDeclarations: true +BinPackArguments: true +BinPackParameters: false +BraceWrapping: + AfterClass: false + AfterControlStatement: false + AfterEnum: false + AfterFunction: false + AfterNamespace: false + AfterObjCDeclaration: false + AfterStruct: false + AfterUnion: false + BeforeCatch: false + BeforeElse: false + IndentBraces: false +BreakBeforeBinaryOperators: None +BreakBeforeBraces: Attach +BreakBeforeTernaryOperators: true +BreakConstructorInitializersBeforeComma: false +BreakAfterJavaFieldAnnotations: false +BreakStringLiterals: true +ColumnLimit: 80 +CommentPragmas: '^ IWYU pragma:' +ConstructorInitializerAllOnOneLineOrOnePerLine: true +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: true +DerivePointerAlignment: false +DisableFormat: false +ExperimentalAutoDetectBinPacking: false +ForEachMacros: [ foreach, Q_FOREACH, BOOST_FOREACH ] +IncludeCategories: + - Regex: '^<.*\.h>' + Priority: 1 + - Regex: '^<.*' + Priority: 2 + - Regex: '.*' + Priority: 3 +IncludeIsMainRegex: '([-_](test|unittest))?$' +IndentCaseLabels: true +IndentWidth: 2 +IndentWrappedFunctionNames: false +JavaScriptQuotes: Leave +JavaScriptWrapImports: true +KeepEmptyLinesAtTheStartOfBlocks: false +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +ObjCBlockIndentWidth: 2 +ObjCSpaceAfterProperty: false +ObjCSpaceBeforeProtocolList: false +PenaltyBreakBeforeFirstCallParameter: 1 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakString: 1000 +PenaltyExcessCharacter: 1000000 +PenaltyReturnTypeOnItsOwnLine: 200 +PointerAlignment: Right +ReflowComments: true +SortIncludes: true +SpaceAfterCStyleCast: false +SpaceAfterTemplateKeyword: true +SpaceBeforeAssignmentOperators: true +SpaceBeforeParens: ControlStatements +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 2 +SpacesInAngles: false +SpacesInContainerLiterals: true +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +Standard: Auto +TabWidth: 8 +UseTab: Never +--- +Language: Proto +BasedOnStyle: Google diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..2df06fd5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +.idea +.vscode +.vs +cmake-build-* +build +out +CMakeSettings.json +compile_commands.json +.cache/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..e26f7c34 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "external/grassland"] + path = external/grassland + url = https://github.com/Yao-class-cpp-studio/grassland-legacy.git +[submodule "external/abseil-cpp"] + path = external/abseil-cpp + url = https://github.com/abseil/abseil-cpp.git diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..916a5bd5 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,14 @@ +repos: + - repo: https://github.com/pre-commit/mirrors-clang-format + rev: v13.0.1 + hooks: + - id: clang-format + files: '\.(glsl|frag|vert|cc|cpp|hpp|h|c|cxx|hxx|rchit|rgen|rmiss|comp|proto)$' + types_or: ['text'] + + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.1.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 00000000..e6809859 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,20 @@ +cmake_minimum_required(VERSION 3.22) + +project(battle_game) +set(CMAKE_CXX_STANDARD 17) + +if (MSVC) + add_compile_options(/utf-8) +endif() + +set(BATTLE_GAME_ASSETS_DIR ${CMAKE_CURRENT_SOURCE_DIR}/assets) + +add_subdirectory(external/grassland) +find_package(absl CONFIG REQUIRED) + +list(APPEND BATTLE_GAME_EXTERNAL_INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/external/abseil-cpp) +list(APPEND BATTLE_GAME_EXTERNAL_INCLUDE_DIRS ${GRASSLAND_INCLUDE_DIRS}) + +add_subdirectory(src) + +target_compile_definitions(battle_game PRIVATE BATTLE_GAME_ASSETS_DIR="${BATTLE_GAME_ASSETS_DIR}/") diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..678c0e84 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 LazyJazz + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 00000000..9ff9429b --- /dev/null +++ b/README.md @@ -0,0 +1,115 @@ +# Project 2: Battle Game + +[![Linux Build](https://github.com/Yao-class-cpp-studio/battle_game/actions/workflows/linux-build.yml/badge.svg)](https://github.com/Yao-class-cpp-studio/battle_game/actions/workflows/linux-build.yml) + +经过一个学期的学习,我们已经一起学习了许多编程知识,大家的编程技能想必也有了显著的提高。 +这个项目将会给大家一个自由展现自己编程能力的平台,为课程画上一个圆满的句号。 +与历次作业和第一次大作业单打独斗的体验不同,这个项目将由参与课程的所有同学共同完成, +你需要和同学们相互配合,让所有人的代码配合到一起,体验共同开发的乐趣。 + +这个项目的目标是制作一款内容丰富的对战小游戏。 +同学们可以自由地发挥自己的创造力,设计各种奇妙有趣的游戏内容,添加绚丽的视觉效果。 +项目结束后,我们的课程团队将会把同学们设计创造的内容进行整合并发布,因此你的设计将有机会被很多人体验到。 +期待大家的成果。 + +## 任务 + +设计本项目的目的是希望大家通过本项目体验和学习软件工程的思维和流程。 + +### 1、创建分叉(Fork)并提交 PR (Pull Request)【5 pts】 + +在一个团队合作的项目中,每个人提交的每一行代码都可能引发无法预料的后果,你的每一行代码在进入主仓库之前都需要经过严格的审核。 +你不拥有对主仓库写权限,在编写你贡献的新代码之前,你需要先从主仓库创建一个属于你自己的分叉(Fork)。在 GitHub 中, +实现这个操作只需要点击仓库主页面右上角的 `Fork` 按钮,并根据跳转页面的提示创建你的分叉。 + +在你的分叉仓库里,你可以对其进行任意的改动,这些都不会影响到主仓库的内容安全。 + +在你写完并提交了新的代码后,你可以通过创建 PR (Pull Request)的方式申请将你分叉仓库中新的代码合并到主仓库。 +你可以通过仓库页面导航栏的 `Pull requests` 按钮进入 PR 页面。 +在 PR 页面内,你可以点击偏右上方绿色的 `New pull request` 按钮进入 Pull Request 创建页面。 +接下来根据页面中的提示和选项,选择你的分叉仓库和你分叉仓库里更新代码的分支(branch)。 +在选择完成后,点击 `Create pull request` 按钮提交你的 PR。 + +这一子任务不需要你提交有意义的代码,你只需要正确地创建分叉,随意提交一些改动,然后正确提交一个 PR 即可,然后请进入下一个子任务。 + +### 2、创建一个新的游戏单位类型 【10 pts】 + +本游戏的框架涉及 4 种类型的元素:单位(Units)、障碍物(Obstacles)、子弹(Bullets)和粒子(Particles)。 +这些元素共同组成了游戏内容。每种类型的元素都有一个基类的定义,所有相应类型的设计实现都应通过继承对应类型元素的基类的方式实现。 +不同类型的元素也有一些符合各自定位的虚函数声明,你在实现的过程中需要给出相应的定义。 +更多框架相关信息的说明请前往 `src/battle_game/core/` 目录下的 [README.md](src/battle_game/core/README.md) 文件查阅。 + +在几种类型的元素中,单位是玩家主要操控的对象,即玩家在游戏内可以扮演的角色。 +这个子任务要求你在项目中创建一个由你进行维护的游戏单位类型,你可以自由地设计你的游戏单位的功能和特性,但你至少需要完成最基础的功能。 +在本次作业发布时,游戏框架已经给出了一个游戏单位的实现范例,即 `src/battle_game/core/units/` 目录下的 `tiny_tank.h,cpp`。 +这个范例实现的是一个坦克单位,可以用键盘进行移动,用鼠标进行瞄准和射击。 +作为最基础的要求,你需要仿制这个坦克单位,并复现其功能。 +如果你希望实现一些别样的游戏单位,你可以自行创造,但你的单位至少需要包含: +正确的外观、合理的移动逻辑以及至少一种射击飞射物(Bullet)的功能。 + +这是你需要做的具体步骤: +1. 在 `src/battle_game/core/units/` 目录下仿照 `tiny_tank.h` 和 `tiny_tank.cpp` 的模式新建一对用于编写你的游戏单位的 `.h` 和 `.cpp` 文件。 +你可以自行选取合适的名字。为了避免和他人的命名产生冲突,你可以在文件名中添加带有个人特征的标记,如:常用网名、学号等。 +2. 在你创建的代码文件里,你需要定义一个新的类型,使其继承自 `Unit` 类型。这个类型名可以任意选取。 +同样的,你可以添加带有个人特征的标记以避免与其他同学冲突。 +3. 实现功能:如果你希望快速获得这部分分数,你可以参考 `tiny_tank` 的实现完成一个你的坦克的功能。 +如果你希望设计创造性的单位,你可以参考 `tiny_tank` 的逻辑设计,编写你自己的单位逻辑。 +4. 在实现完成你的单位类型后,给你的作品签上自己的名字吧!你需要重载 `UnitName` 和 `Author` 两个函数, +前者是你创造的单位的名称,后者是作者的名称,也就是你的名字,你可以随意地用真实姓名或者网名。 +5. 最后你需要将定义了你的单位的头文件添加到 `src/battle_game/core/units/units.h` 中, +然后前往 `src/battle_game/core/selectable_units.cpp` 将你的单位添加到可选单位列表。 + + +一切做完后,编译并运行程序,你会看到你的单位已经出现在左上角的可选单位列表中了。你可以从列表中选择你的单位, +用 `自毁` 按钮消灭当前默认创建的单位,等待 5 秒后重生出的新单位就是你的作品了! + +如果你的单位运行起来一切正常,你可以将你的代码推送到你的分叉仓库中,你之前创建的 PR 会自动同步你更新的内容。 +这时候你就可以找助教验收这个子任务的成果了。 + +如果你发现你的 PR 被 clang-format 检查卡住了,这是因为你的代码风格不标准。 +你不需要手调代码风格,这样会很累。你可以采取如下自动的方式: + +1. 下载 pre-commit: +```shell +pip install pre-commit +``` +2. 将 pre-commit 的钩子加入到 git commit 中 +```shell +pre-commit install # 在仓库主目录下运行 +``` +3. 重新提交你修改的文件: +```shell +git commit -am "Your Comments" +``` +此时 pre-commit 会帮助你自动修改代码风格。 +如果报错了,就将上放指令再敲一遍,直到提交成功。 + +4. 推送到远端仓库 + +### 3、提出一个新功能的想法到 Issues 列表 【5 pts】 + +有时你有一个很好的想法,但是没有办法立刻实现, +或者你发现项目中存在一个 Bug,希望以后去修复。 +这时你可以利用 GitHub 的 Issues 功能先把问题记下来。 + +项目的构建需要大家发挥自己的想象力和创造力, +你既是乙方,也是甲方。 +这个子任务要求你在 GitHub Issues 栏中提交一个有效的 Issue。 +你需要在 Issue 中尽可能详细的说明问题的需求,最好同时给出一个可行的实现思路。 +如果前面已经有同学提过类似或者相同的问题,那就不要再重复提交了。 +我们不在意你提的问题是复杂还是简单,只要是一个有效的问题,你都可以直接拿到这个子任务的 5 分! + +### 4、自由创造 【10 pts】 + +在同学们提出很多好的 idea 后,我们希望能够把他们一一变为现实。 +最后的这 10 分完全由你自行发挥。 +你可以从 Issues 列表中选择一个你认为自己可以解决的部分,你可以申领这个 Issue,然后提交 PR 实现并关闭之。 +你可以和同学合作完成,在提交 PR 时请说明每个人贡献了哪部分代码。 +每一个被成功解决的 Issue 的所有参与者都可以获得 5 分本子任务的分数,当然,满分 10 分封顶。 +你如果希望多做一些贡献那就更好了,毕竟,这个项目就是为了大家能够体会编程的乐趣! +Good Luck and Have Fun! + +## 后记 + +我们希望同学们通过这次作业体会到编程的乐趣,在过程中创造快乐,最终我们会一起呈现一个完整的作品,这离不开每一位同学的贡献。 +你的每一行代码都是重要的,也是有意义的。希望多年以后,这段旅程能够成为同学们共同的美好回忆。 diff --git a/assets/fonts/NotoSansSC-Black.otf b/assets/fonts/NotoSansSC-Black.otf new file mode 100644 index 00000000..7529643c Binary files /dev/null and b/assets/fonts/NotoSansSC-Black.otf differ diff --git a/assets/fonts/NotoSansSC-Bold.otf b/assets/fonts/NotoSansSC-Bold.otf new file mode 100644 index 00000000..172eb674 Binary files /dev/null and b/assets/fonts/NotoSansSC-Bold.otf differ diff --git a/assets/fonts/NotoSansSC-Regular.otf b/assets/fonts/NotoSansSC-Regular.otf new file mode 100644 index 00000000..d350ffa7 Binary files /dev/null and b/assets/fonts/NotoSansSC-Regular.otf differ diff --git a/assets/textures/particle0.png b/assets/textures/particle0.png new file mode 100644 index 00000000..80b2c13c Binary files /dev/null and b/assets/textures/particle0.png differ diff --git a/assets/textures/particle1.png b/assets/textures/particle1.png new file mode 100644 index 00000000..8bf4bd1c Binary files /dev/null and b/assets/textures/particle1.png differ diff --git a/assets/textures/particle2.png b/assets/textures/particle2.png new file mode 100644 index 00000000..fcb18d09 Binary files /dev/null and b/assets/textures/particle2.png differ diff --git a/assets/textures/particle3.png b/assets/textures/particle3.png new file mode 100644 index 00000000..0121fa1c Binary files /dev/null and b/assets/textures/particle3.png differ diff --git a/external/grassland b/external/grassland new file mode 160000 index 00000000..49192243 --- /dev/null +++ b/external/grassland @@ -0,0 +1 @@ +Subproject commit 49192243e390e7a00f6db608e10d61d8c9e49376 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 00000000..544e1783 --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,4 @@ + +set(BATTLE_GAME_INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}) + +add_subdirectory(battle_game) diff --git a/src/battle_game/CMakeLists.txt b/src/battle_game/CMakeLists.txt new file mode 100644 index 00000000..8f88d09b --- /dev/null +++ b/src/battle_game/CMakeLists.txt @@ -0,0 +1,13 @@ +file(GLOB files *) + +foreach(file ${files}) + if (IS_DIRECTORY ${file}) + file(RELATIVE_PATH short_name ${CMAKE_CURRENT_SOURCE_DIR} ${file}) + set(COMPONENT_NAME ${short_name}) + add_subdirectory(${file}) + endif() +endforeach() + +add_executable(battle_game battle_game.cpp) +target_link_libraries(battle_game PUBLIC ${LIBRARY_LIST}) +target_include_directories(battle_game PUBLIC ${BATTLE_GAME_EXTERNAL_INCLUDE_DIRS} ${BATTLE_GAME_INCLUDE_DIR}) diff --git a/src/battle_game/app/CMakeLists.txt b/src/battle_game/app/CMakeLists.txt new file mode 100644 index 00000000..4627eb43 --- /dev/null +++ b/src/battle_game/app/CMakeLists.txt @@ -0,0 +1,14 @@ +set(lib_name battle_game_${COMPONENT_NAME}_lib) +list(APPEND LIBRARY_LIST ${lib_name}) + +add_library(${lib_name}) +file(GLOB source_files *.cpp *.h) +target_sources(${lib_name} PUBLIC ${source_files}) +target_link_libraries(${lib_name} PUBLIC grassland) +target_include_directories(${lib_name} PUBLIC ${BATTLE_GAME_EXTERNAL_INCLUDE_DIRS} ${BATTLE_GAME_INCLUDE_DIR}) + +set(LIBRARY_LIST ${LIBRARY_LIST} PARENT_SCOPE) + +target_compile_definitions(${lib_name} PUBLIC BATTLE_GAME_ASSETS_DIR="${BATTLE_GAME_ASSETS_DIR}/") + +PACK_SHADER_CODE(${lib_name}) diff --git a/src/battle_game/app/app.cpp b/src/battle_game/app/app.cpp new file mode 100644 index 00000000..7292211c --- /dev/null +++ b/src/battle_game/app/app.cpp @@ -0,0 +1,328 @@ +#include "battle_game/app/app.h" + +#include + +#include "battle_game/core/object.h" +#include "battle_game/graphics/util.h" + +namespace { +#include "built_in_shaders.inl" +} + +namespace battle_game { +static void HelpMarker(const char *desc) { + ImGui::TextColored(ImVec4(1, 1, 0, 1), "(?)"); + if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayShort)) { + ImGui::BeginTooltip(); + ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f); + ImGui::TextUnformatted(desc); + ImGui::PopTextWrapPos(); + ImGui::EndTooltip(); + } +} + +App::App(const AppSettings &app_settings, GameCore *game_core) { + game_core_ = game_core; + vulkan_legacy::framework::CoreSettings core_settings; + core_settings.window_title = "Battle Game"; + core_settings.window_width = app_settings.width; + core_settings.window_height = app_settings.height; + core_ = std::make_unique(core_settings); + SetGlobalCore(core_.get()); +} + +void App::Run() { + OnInit(); + + while (!glfwWindowShouldClose(core_->GetWindow())) { + OnLoop(); + glfwPollEvents(); + } + + core_->GetDevice()->WaitIdle(); + + OnClose(); +} + +void App::OnInit() { + frame_image_ = std::make_unique( + core_.get(), core_->GetFramebufferWidth(), core_->GetFramebufferHeight(), + VK_FORMAT_B8G8R8A8_UNORM); + + core_->SetFrameSizeCallback([this](int width, int height) { + frame_image_->Resize(width, height); + BuildRenderNodes(); + }); + + device_global_settings_ = + std::make_unique>( + core_.get(), 1); + device_texture_infos_ = + std::make_unique>( + core_.get(), 1048576); + device_object_settings_ = + std::make_unique>( + core_.get(), 1048576); + + linear_sampler_ = std::make_unique(core_->GetDevice(), + VK_FILTER_LINEAR); + nearest_sampler_ = std::make_unique( + core_->GetDevice(), VK_FILTER_NEAREST); + + SyncDeviceAssets(); + BuildRenderNodes(); + + SetScene(); + core_->ImGuiInit(frame_image_.get(), + BATTLE_GAME_ASSETS_DIR "fonts/NotoSansSC-Regular.otf", + 20.0f); +} + +void App::OnLoop() { + OnUpdate(); + OnRender(); +} + +void App::OnClose() { +} + +void App::OnUpdate() { + UpdateImGui(); + NewFrame(); + CaptureInput(); + UpdateDrawCommands(); + UpdateDynamicBuffer(); + + auto mgr = AssetsManager::GetInstance(); + if (!mgr->GetSyncState()) { + SyncDeviceAssets(); + } +} + +void App::OnRender() { + core_->BeginCommandRecord(); + frame_image_->ClearColor({0.8f, 0.8, 0.8f, 1.0f}); + + auto &model_ids = GetModelIds(); + render_node_->BeginDraw(); + for (int obj_id = 0; obj_id < model_ids.size(); obj_id++) { + auto &device_model = device_models_[model_ids[obj_id]]; + render_node_->DrawDirect(device_model.vertex_buffer.get(), + device_model.index_buffer.get(), + device_model.index_buffer->Size(), obj_id); + } + render_node_->EndDraw(); + core_->TemporalSubmit(); + core_->ImGuiRender(); + core_->Output(frame_image_.get()); + core_->EndCommandRecordAndSubmit(); +} + +void App::BuildRenderNodes() { + render_node_ = + std::make_unique(core_.get()); + render_node_->AddColorAttachment(frame_image_.get(), true); + + auto vertex_shader = vulkan::CompileGLSLToSPIRV( + GetShaderCode("shaders/render.vert"), VK_SHADER_STAGE_VERTEX_BIT); + auto fragment_shader = vulkan::CompileGLSLToSPIRV( + GetShaderCode("shaders/render.frag"), VK_SHADER_STAGE_FRAGMENT_BIT); + { + std::ofstream vertex_shader_file("render.vert.spv", std::ios::binary); + vertex_shader_file.write((const char *)vertex_shader.data(), + vertex_shader.size() * sizeof(uint32_t)); + vertex_shader_file.close(); + + std::ofstream fragment_shader_file("render.frag.spv", std::ios::binary); + fragment_shader_file.write((const char *)fragment_shader.data(), + fragment_shader.size() * sizeof(uint32_t)); + fragment_shader_file.close(); + } + + render_node_->AddShader("render.vert.spv", VK_SHADER_STAGE_VERTEX_BIT); + render_node_->AddShader("render.frag.spv", VK_SHADER_STAGE_FRAGMENT_BIT); + render_node_->VertexInput({VK_FORMAT_R32G32_SFLOAT, VK_FORMAT_R32G32_SFLOAT, + VK_FORMAT_R32G32B32A32_SFLOAT}); + render_node_->AddBufferBinding( + device_texture_infos_.get(), + VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT); + std::vector> + texture_sampler_pairs; + for (auto &device_particle_texture : device_particle_textures_) { + texture_sampler_pairs.emplace_back(device_particle_texture.get(), + linear_sampler_.get()); + } + render_node_->AddUniformBinding( + texture_sampler_pairs, + VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT); + render_node_->AddUniformBinding( + device_global_settings_.get(), + VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT); + render_node_->AddBufferBinding( + device_object_settings_.get(), + VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT); + render_node_->BuildRenderNode(); +} + +void App::SyncDeviceAssets() { + auto mgr = AssetsManager::GetInstance(); + auto &particle_textures = mgr->GetTextures(); + while (device_particle_textures_.size() < particle_textures.size()) { + auto &particle_texture = + particle_textures[device_particle_textures_.size()]; + device_particle_textures_.push_back( + std::make_unique( + core_.get(), particle_texture.GetWidth(), + particle_texture.GetHeight())); + } + for (int i = 0; i < device_particle_textures_.size(); i++) { + auto &particle_texture = particle_textures[i]; + auto host_buffer = particle_texture.GetBuffer(); + vulkan_legacy::Buffer upload_buffer( + core_->GetDevice(), + sizeof(glm::vec4) * particle_texture.GetWidth() * + particle_texture.GetHeight(), + VK_BUFFER_USAGE_TRANSFER_SRC_BIT, + VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | + VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); + std::memcpy(upload_buffer.Map(), host_buffer, + sizeof(glm::vec4) * particle_texture.GetWidth() * + particle_texture.GetHeight()); + upload_buffer.Unmap(); + vulkan_legacy::UploadImage(core_->GetCommandPool(), + device_particle_textures_[i]->GetImage(), + &upload_buffer); + } + + auto &models = mgr->GetModels(); + while (device_models_.size() < models.size()) { + auto &model = models[device_models_.size()]; + auto &vertices = model.GetVertices(); + auto &indices = model.GetIndices(); + device_models_.push_back( + {std::make_unique>( + core_.get(), vertices.size()), + std::make_unique>( + core_.get(), indices.size())}); + auto &device_model = *device_models_.rbegin(); + device_model.vertex_buffer->Upload(vertices.data()); + device_model.index_buffer->Upload(indices.data()); + } + + mgr->GetSyncState() = true; +} + +void App::UpdateDrawCommands() { + static auto begin_time = std::chrono::steady_clock::now(); + auto current_time = std::chrono::steady_clock::now(); + auto time_passed = + double((current_time - begin_time) / std::chrono::nanoseconds(1)) * 1e-9; + static uint64_t updated_step = 0; + uint64_t target_update_step = std::lround(time_passed / kSecondPerTick); + while (updated_step < target_update_step) { + game_core_->Update(); + updated_step++; + } + game_core_->Render(); +} + +void App::UpdateDynamicBuffer() { + auto &object_settings = GetObjectSettings(); + auto &texture_infos = GetTextureInfos(); + std::memcpy(&(*device_object_settings_)[0], object_settings.data(), + object_settings.size() * sizeof(ObjectSettings)); + std::memcpy(&(*device_texture_infos_)[0], texture_infos.data(), + texture_infos.size() * sizeof(TextureInfo)); + (*device_global_settings_)[0].world_to_camera = GetCameraTransform(fov_y_); +} + +void App::CaptureInput() { + InputData input_data; + auto window = core_->GetWindow(); + for (int i = 0; i < kKeyRange; i++) { + input_data.key_down[i] = (glfwGetKey(window, i) == GLFW_PRESS); + } + static bool mouse_button_state[kMouseButtonRange] = {}; + for (int i = 0; i < kMouseButtonRange; i++) { + if (i == GLFW_MOUSE_BUTTON_LEFT && ImGui::GetIO().WantCaptureMouse) { + input_data.mouse_button_down[i] = false; + continue; + } + input_data.mouse_button_down[i] = + (glfwGetMouseButton(window, i) == GLFW_PRESS); + input_data.mouse_button_clicked[i] = + (input_data.mouse_button_down[i] && !mouse_button_state[i]); + mouse_button_state[i] = input_data.mouse_button_down[i]; + } + double xpos, ypos; + glfwGetCursorPos(window, &xpos, &ypos); + int width, height; + glfwGetWindowSize(window, &width, &height); + xpos += 0.5f; + ypos += 0.5f; + input_data.mouse_cursor_position = + glm::inverse(GetCameraTransform(fov_y_)) * + glm::vec4{ + (glm::vec2{xpos, ypos} / glm::vec2{width, height}) * 2.0f - 1.0f, + 0.0f, 1.0f}; + game_core_->GetPlayer(my_player_id_)->SetInputData(input_data); +} + +void App::SetScene() { + my_player_id_ = game_core_->AddPlayer(); + auto enemy_player_id = game_core_->AddPlayer(); + game_core_->SetRenderPerspective(my_player_id_); +} + +glm::mat4 App::GetCameraTransform(float fov_y) const { + auto inv_fov_y = 1.0f / fov_y; + return glm::scale( + glm::mat4{1.0f}, + glm::vec3{inv_fov_y / ((float)core_->GetFramebufferWidth() / + (float)core_->GetFramebufferHeight()), + -inv_fov_y, 1.0f}) * + glm::inverse( + glm::translate(glm::mat4{1.0f}, + glm::vec3{game_core_->GetCameraPosition(), 0.0f}) * + glm::rotate(glm::mat4{1.0f}, game_core_->GetCameraRotation(), + glm::vec3{0.0f, 0.0f, 1.0f})); +} + +void App::UpdateImGui() { + ImGui_ImplVulkan_NewFrame(); + ImGui_ImplGlfw_NewFrame(); + ImGui::NewFrame(); + ImGui::SetNextWindowPos(ImVec2{0.0f, 0.0f}, ImGuiCond_Once); + ImGui::SetNextWindowBgAlpha(0.3f); + if (ImGui::Begin(u8"调试窗口", nullptr, + ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar | + ImGuiWindowFlags_AlwaysAutoResize)) { + auto player = game_core_->GetPlayer(my_player_id_); + if (player) { + auto selectable_list = game_core_->GetSelectableUnitList(); + auto selectable_list_skill = game_core_->GetSelectableUnitListSkill(); + ImGui::Combo(u8"选择你的单位(重生后生效)", &player->SelectedUnit(), + selectable_list.data(), selectable_list.size()); + if (ImGui::Button(u8"自毁")) { + auto unit = game_core_->GetUnit(player->GetPrimaryUnitId()); + if (unit) { + game_core_->PushEventRemoveUnit(unit->GetId()); + } + } + auto unit = game_core_->GetUnit(player->GetPrimaryUnitId()); + if (unit) { + ImGui::Text(u8"生命值: %.1f / %.1f", + unit->GetHealth() * unit->GetMaxHealth(), + unit->GetMaxHealth()); + ImGui::ProgressBar(unit->GetHealth()); + } else { + ImGui::Text(u8"已死亡,等待%d秒后复活。", + player->GetResurrectionCountDown() / kTickPerSecond); + } + } + ImGui::End(); + } + ImGui::Render(); +} +} // namespace battle_game diff --git a/src/battle_game/app/app.h b/src/battle_game/app/app.h new file mode 100644 index 00000000..7b1f811b --- /dev/null +++ b/src/battle_game/app/app.h @@ -0,0 +1,56 @@ +#pragma once +#include "battle_game/app/app_settings.h" +#include "battle_game/app/device_model.h" +#include "battle_game/core/game_core.h" +#include "battle_game/graphics/graphics.h" +#include "grassland/grassland.h" + +namespace battle_game { +using namespace grassland; +class App { + public: + explicit App(const AppSettings &app_settings, GameCore *game_core); + void Run(); + + private: + void OnInit(); + void OnLoop(); + void OnClose(); + + void OnUpdate(); + void OnRender(); + + void BuildRenderNodes(); + void SyncDeviceAssets(); + + void CaptureInput(); + void UpdateDrawCommands(); + void UpdateDynamicBuffer(); + void UpdateImGui(); + + void SetScene(); + [[nodiscard]] glm::mat4 GetCameraTransform(float fov_y) const; + + GameCore *game_core_{nullptr}; + + std::unique_ptr core_; + std::unique_ptr frame_image_; + + std::vector device_models_; + std::vector> + device_particle_textures_; + std::unique_ptr> + device_global_settings_; + std::unique_ptr> + device_texture_infos_; + std::unique_ptr> + device_object_settings_; + std::unique_ptr linear_sampler_; + std::unique_ptr nearest_sampler_; + + std::unique_ptr render_node_; + + uint32_t my_player_id_{0}; + float fov_y_{10.0f}; +}; +} // namespace battle_game diff --git a/src/battle_game/app/app_settings.h b/src/battle_game/app/app_settings.h new file mode 100644 index 00000000..6967bf51 --- /dev/null +++ b/src/battle_game/app/app_settings.h @@ -0,0 +1,9 @@ +#pragma once +#include "cstdint" + +namespace battle_game { +struct AppSettings { + uint32_t width{1280}; + uint32_t height{720}; +}; +} // namespace battle_game diff --git a/src/battle_game/app/device_model.h b/src/battle_game/app/device_model.h new file mode 100644 index 00000000..49d81be8 --- /dev/null +++ b/src/battle_game/app/device_model.h @@ -0,0 +1,12 @@ +#pragma once +#include "battle_game/graphics/util.h" +#include "grassland/grassland.h" + +namespace battle_game { +struct DeviceModel { + std::unique_ptr> + vertex_buffer; + std::unique_ptr> + index_buffer; +}; +} // namespace battle_game diff --git a/src/battle_game/app/shaders/object_structs.glsl b/src/battle_game/app/shaders/object_structs.glsl new file mode 100644 index 00000000..7969f3d1 --- /dev/null +++ b/src/battle_game/app/shaders/object_structs.glsl @@ -0,0 +1,17 @@ + +struct GlobalSettings { + mat4 world_to_camera; +}; + +struct ObjectSettings { + mat4 local_to_world; + vec4 color; +}; + +struct TextureInfo { + float x; + float y; + float width; + float height; + int texture_id; +}; diff --git a/src/battle_game/app/shaders/render.frag b/src/battle_game/app/shaders/render.frag new file mode 100644 index 00000000..4cde9135 --- /dev/null +++ b/src/battle_game/app/shaders/render.frag @@ -0,0 +1,35 @@ +#version 460 +#extension GL_GOOGLE_include_directive : require +#extension GL_EXT_nonuniform_qualifier : enable + +// clang-format off +#include "object_structs.glsl" +// clang-format on + +layout(location = 0) in flat int instance_index; +layout(location = 1) in vec2 frag_tex_coord; +layout(location = 2) in vec4 frag_color; +layout(location = 0) out vec4 color_output; + +layout(binding = 0) buffer texture_info_array { + TextureInfo texture_infos[]; +}; +layout(binding = 1) uniform sampler2D[] texture_samplers; +layout(binding = 2) uniform global_settings_object { + GlobalSettings global_settings; +}; +layout(binding = 3) buffer object_settings_array { + ObjectSettings object_settings[]; +}; + +void main() { + TextureInfo texture_info = texture_infos[instance_index]; + vec2 tex_size = + vec2(textureSize(texture_samplers[texture_info.texture_id], 0)); + color_output = + frag_color * object_settings[instance_index].color * + texture(texture_samplers[texture_info.texture_id], + (vec2(texture_info.x, texture_info.y) + + frag_tex_coord * vec2(texture_info.width, texture_info.height)) / + vec2(tex_size)); +} diff --git a/src/battle_game/app/shaders/render.vert b/src/battle_game/app/shaders/render.vert new file mode 100644 index 00000000..afed0f54 --- /dev/null +++ b/src/battle_game/app/shaders/render.vert @@ -0,0 +1,36 @@ +#version 460 +#extension GL_GOOGLE_include_directive : require +#extension GL_EXT_nonuniform_qualifier : enable + +// clang-format off +#include "object_structs.glsl" +// clang-format on + +layout(location = 0) in vec3 position; +layout(location = 1) in vec2 tex_coord; +layout(location = 2) in vec4 color; +layout(location = 0) out flat int instance_index; +layout(location = 1) out vec2 frag_tex_coord; +layout(location = 2) out vec4 frag_color; + +layout(binding = 0) buffer texture_info_array { + TextureInfo texture_infos[]; +}; +layout(binding = 1) uniform sampler2D[] texture_samplers; +layout(binding = 2) uniform global_settings_object { + GlobalSettings global_settings; +}; +layout(binding = 3) buffer object_settings_array { + ObjectSettings object_settings[]; +}; + +void main() { + instance_index = gl_InstanceIndex; + frag_tex_coord = tex_coord; + frag_color = color; + ObjectSettings object_info = object_settings[instance_index]; + gl_Position = vec4(0.0, 0.0, 1.0, 0.0) + + (global_settings.world_to_camera * object_info.local_to_world * + vec4(position.xy, 0.5, 1.0)) * + vec4(1.0, 1.0, -1.0, 1.0); +} diff --git a/src/battle_game/battle_game.cpp b/src/battle_game/battle_game.cpp new file mode 100644 index 00000000..85e668bc --- /dev/null +++ b/src/battle_game/battle_game.cpp @@ -0,0 +1,10 @@ +#include "battle_game/app/app.h" +#include "battle_game/core/game_core.h" +#include "battle_game/graphics/graphics.h" + +int main() { + battle_game::GameCore game_core; + battle_game::AppSettings app_settings; + battle_game::App app(app_settings, &game_core); + app.Run(); +} diff --git a/src/battle_game/core/CMakeLists.txt b/src/battle_game/core/CMakeLists.txt new file mode 100644 index 00000000..31eaad75 --- /dev/null +++ b/src/battle_game/core/CMakeLists.txt @@ -0,0 +1,12 @@ +set(lib_name battle_game_${COMPONENT_NAME}_lib) +list(APPEND LIBRARY_LIST ${lib_name}) + +add_library(${lib_name}) +file(GLOB_RECURSE source_files *.cpp *.h) +target_sources(${lib_name} PUBLIC ${source_files}) +target_link_libraries(${lib_name} PUBLIC grassland) +target_include_directories(${lib_name} PUBLIC ${BATTLE_GAME_EXTERNAL_INCLUDE_DIRS} ${BATTLE_GAME_INCLUDE_DIR}) + +set(LIBRARY_LIST ${LIBRARY_LIST} PARENT_SCOPE) + +target_compile_definitions(${lib_name} PRIVATE BATTLE_GAME_ASSETS_DIR="${BATTLE_GAME_ASSETS_DIR}/") diff --git a/src/battle_game/core/README.md b/src/battle_game/core/README.md new file mode 100644 index 00000000..4d2d4823 --- /dev/null +++ b/src/battle_game/core/README.md @@ -0,0 +1,295 @@ +# Core + +这个文件夹下的内容是整个项目的核心框架。 +[GameCore](game_core.h) 类是所有游戏逻辑集中协调的地方。 + +在这个框架中,游戏内容主要分为四类元素:单位([Units](unit.h))、障碍物([Obstacles](obstacle.h))、子弹([Bullets](bullet.h))和粒子([Particles](particle.h))。 +这四类元素的基类统一继承自对象类型([Object](object.h)),他们都有一定特定的预设功能,用于方便你实现自己的想法。 + +## Object + +对象类的声明位于 [object.h](object.h) 文件中, +你可以看到其包含了以下成员变量: + +```c++ +class Object { + ... +protected: + GameCore *game_core_{nullptr}; + glm::vec2 position_{0.0f}; // offset from the origin (0, 0) + float rotation_{0.0f}; // angle in radians + uint32_t id_{0}; +}; +``` + +- game_core_ + - 这是一个指向游戏核心类的指针,会在对象被创建时赋值。当对象在运行过程中需要与游戏内其他元素交互时, +你可以通过这个指针调用游戏核心的相应功能函数。 + - 约定:被用于游戏中的对象,game_core_ 一定指向合法的对象,且在过程中不更改其值。 + - 约定:game_core_ 为 nullptr 时,则当前对象是在进行一些游戏内容外的测试,此时该对象游戏逻辑部分代码不会也不应该被调用。 +- id_ + - 表示对象的编号,同类元素的不同实体拥有不同的编号,不同类型元素的实体可能拥有相同编号。 + 例如,属于单位 Unit 子类的实体,本变量表示的值为**单位编号**(Unit ID),以此为类比,适用于其他三类元素。 + - 约定:任何类型元素的编号 0 为保留编号,用于一些特殊判断的处理。 +- position_, rotation_ + - 游戏中每一个对象都拥有其位置信息 + - position_ 表示对象相对于游戏内坐标原点的偏移量 + - glm::vec2 是 glm 数学运算库中表示单精度浮点二维向量的类型,这个数学库内包含了四维及以下向量、矩阵的类型定义和常用功能函数。 + - rotation_ 表示对象在场景内相比于默认朝向,逆时针旋转的角度 + +除成员变量外,所有对象都应包含两部分功能: + +```c++ +class Object { +public: + ... + virtual void Update() = 0; + virtual void Render() = 0; +}; +``` + + +- Update + - 这个函数用于更新对象的状态 + - 对象的主要逻辑代码都应写在这里 + - 这个函数会在每一个游戏帧(Tick)更新时被调用 +- Render + - 这个函数用于绘制对象的视觉效果 + - 你可以调用 `src/battle_game/graphics/grahics.h` 中的函数进行画面的绘制, + 前往 [src/battle_game/graphics/README.md](../graphics/README.md) 查看相关绘图函数的使用方法。 + +为了方便构思,我们可以想象所有对象都根据其位置信息拥有一个自己的**本地空间**坐标系, +相对于**世界空间**坐标系,在**本地空间**坐标系上进行计算会带来许多便利。 +因此 Object 类还提供了如下两个函数用于辅助计算 + +- LocalToWorld + - 传入参数为**本地空间**坐标 + - 返回对应的**世界空间**坐标 +- WorldToLocal + - 传入参数为**世界空间**坐标 + - 返回对应的**本地空间**坐标 + +## Unit + +单位类声明在 [unit.h](unit.h) 中,单位表示游戏中进行主动动作的对象,所有具体单位的实现都应继承自此基类。 + +### 成员变量 + +- player_id_ + - 这个变量表示单位所有者的**玩家编号**(Player ID),用于区分敌我 +- health_ + - 这个变量表示单位的生命值 + - 取值范围为 [0, 1],即剩余生命相对于最大生命值生命值的比例。 + - 单位实际生命值为 `GetMaxHealth() * health_` + - 这样定义是为了方便动态地对最大声明值进行调整,以实现一些复杂机制。 + - 该值归 0 时单位死亡。 +- lifebar_* + - 这些变量保存了生命条的设置,请通过set来修改 +- fadeout_health_ + - 保存生命条渐变的起始位置。(一般不需要修改) + +### 成员函数 + +- SetPosition + - 设置单位位置的函数 + - 为了游戏帧更新时的数据一致性,一般不直接调用,而是用 GameCore 中的 PushEventMoveUnit 函数添加单位移动事件 +- SetRotation + - 设置单位朝向的函数 + - 为了游戏帧更新时的数据一致性,一般不直接调用,而是用 GameCore 中的 PushEventRotateUnit 函数添加单位旋转事件 +- GetDamageScale + - 获取单位的伤害倍率,默认为 1.0 + - 你可以编写这部分的计算逻辑以实现高倍率攻击力属性、光环等效果 + - 单位造成伤害的功能都应考虑此函数的影响 +- GetSpeedScale + - 获取单位的移动速度倍率,默认为 1.0 + - 你可以编写这部分的计算逻辑以实现加速、减速相关的属性、光环功能 +- BasicMaxHealth + - 这是一个虚函数 + - 最大生命值基准,默认为 100.0 + - 表示生命值基础数值 + - 你可以在不同单位的实现中通过 `override` 修改单位的基础生命值 +- GetHealthScale + - 生命值倍率,默认为 1.0 + - 你可以编写这部分的计算逻辑以实现增强或衰弱生命值的功能 +- GetMaxHealth + - 最大生命值 + - 定义为基础生命值乘以生命值倍率 +- Set/GetLifeBar* + - 修改/获取各种生命条设置 +- RenderLifeBar + - 这是一个虚函数 + - 渲染该对象对应的生命条 +- Hide/ShowLifeBar + - 隐藏/显示生命条 +- RenderHelper + - 这是一个虚函数 + - 仅在该单位所有者玩家的视角中,渲染该对象用于辅助的一些视觉效果(例如子弹射出的预计轨迹) +- IsHit + - 这是一个虚函数 + - 用于判断若一个事件发生于传入参数**世界空间**坐标 `position`,该事件是否会对当前单位产生影响 + - 可以理解为定义命中体积的函数 + - 通常用于判断子弹、飞射物是否命中了当前单位 + - 你可以在子类实现中通过 `override` 定义不同的命中判断逻辑 +- GenerateBullet + - 这是一个模板函数 + - 由于子弹类的生成包含了许多类似于发出者**单位编号**、所属**玩家编号**相关的固定信息,我们希望把基本信息相关的内容省略,从而只通过传入关键信息即可生成一颗子弹, + 该函数可以帮助你通过只填写新生成的子弹对象的位置、朝向、伤害倍率等关键参数,自动根据单位信息进行补全并添加一个子弹生成事件。 + - 实现类似于“开火”一类的技能可以使用该函数 + - 函数的实现位于 `src/battle_game/core/game_core.h` 中 +- Skill + - units支持加入技能。 + - 为了方便玩家操作,技能应当使用键盘快捷键完成。特别地,由于本游戏使用W/A/S/D控制转向,为方便起见,技能采用按键E/Q/R完成。我们规定E/Q/R表示的技能强度递增,并建议按照E/Q/R的顺序依次实现技能(可不足3个,但主动技能一般不会超过3个)。此外,P表示被动技能,这一技能不需要用户输入。 + - 用户通常希望从UI界面获取技能的简略信息。因此,如果您不希望技能被展示在UI界面中,请使用ADD_SELECTABLE_UNIT_WITHOUT_SKILL()进行调用(如果您没有设置技能,则不会显示任何信息。因此技能界面向前兼容)。此外,您需要维护一个名称为skill_的信息存储库,它已经是您的units类型中的protected类型。它的格式为std::vector 。其中Skill是一个用于交互的结构体。 + - units支持子弹切换界面显示。一个units可以拥有不止一种射击的子弹,并且子弹切换通常需要一定的冷却时间。如果您不希望展示子弹界面,您只需要留空即可。 如果您希望展示子弹界面,请在skill_里加入type=B的一个元素。如果有不止一个子弹,请不要多次添加,而是填写当前的bullet_type和一共的bullet_total_number。子弹界面对您的输入具有一定的适应性,例如,如果您只有一种子弹,将不会展示切换信息;如果您的冷却时间为0,将不会展示冷却进度。 +``` cpp +enum SkillType { E, Q, R, P, B }; +struct Skill { + std::string name; + std::string description; + std::string src; + uint32_t time_remain; + uint32_t time_total; + uint32_t bullet_type; + uint32_t bullet_total_number; + SkillType type; + std::function function; +}; +``` + + - + - 你需要在name中填写技能名称,description为技能简述(若有),src为技能图示路径(若有),time_remain为技能冷却时间,time_total为技能冷却总时间,type为技能类型,function为技能调用的接口(可选择不提供)。若选择提供,使用格式为example.function=SKILL_ADD_FUNCTION(YourUnits::YourFunction)。 + - 使用示例请参考inferno_tank类型。技能显示页面可能会持续更新,但可以承诺skill_这一交互容器会保持不变。也即技能显示页面的更新会自动兼容您的数据,您无须再次编写。如果您发现了显示页面的BUG或者希望增加更多内容(如您可能希望加入用户状态,如加速/灼烧等),欢迎联系XuGW-Kevin。 +## Obstacle + +障碍物类声明在 [obstacle.h](obstacle.h) 中,该类对象主要用于组成游戏场景。 + +- GetSurfaceNormal + - 给定向量的始点与终点信息,返回向量与物体表面相交处对应的表面单位法向量信息。我们约定其返回值的第一项为给定线段与表面的交点,第二项为单位法向量的方向。 + - 主要用于实现子弹的反弹。对于障碍物(对于某些子弹)不应该返回对应表面单位法向量的情况,我们约定返回值中单位法向量的方向设为 (0,0)。 + +### 成员函数 + +- IsBlocked + - 这是一个虚函数 + - 该函数用于判断传入参数**世界空间**坐标 `position` 是否被该障碍物阻挡 + - 你可以在子类实现中通过 `override` 定义不同的障碍物作用模式 + +## Bullet + +子弹类声明在 [bullet.h](bullet.h) 文件中,该类对象主要用于表示会对游戏性产生影响的临时对象。如:子弹 + +相比于子弹这个名称,或许用“飞射物”一词更符合其广义的含义。 + +### 成员变量 + +- unit_id_ + - 表示该飞射物的创建者的**单位编号**(Unit ID),用于可能实现的“经验值系统”或相关机制 +- player_id_ + - 表示该飞射物的所属玩家的**玩家编号**(Player ID),用于区分阵营 +- damage_scale_ + - 伤害倍率,用于存储该子弹被创建时被赋予的伤害倍率,以作用于命中时的实际伤害 + + +## Particle + +粒子类声明在 [particle.h](particle.h) 文件中,该类对象主要用于表示不会对游戏性产生影响的临时对象,用于提升视觉体验。 + +## GameCore + +游戏核心提供了许多用于方便游戏内元素进行交互的函数功能 + +### 对象访问 + +通过游戏核心对象你可以访问其他任意仍旧存在于游戏中的对象: + +- GetUnit + - 根据输入的**单位编号**(Unit ID),返回对应单位对象的指针 + - 若**单位编号**指向的单位已经被删除或编号不合法,则返回 `nullptr` +- GetUnits + - 获取所有现存的单位 +- GetObstacle + - 根据输入的**障碍物编号**(Obstacle ID),返回对应障碍物对象的指针 + - 若**障碍物编号**指向的障碍物已经被删除或编号不合法,则返回 `nullptr` +- GetObstacles + - 获取所有现存的障碍物 +- GetBlockedObstacle + - 判断传入参数**世界空间**坐标 `position` 是否被某障碍物阻挡。如果是,返回对应障碍物对象的指针 +- GetBullet + - 根据输入的**子弹编号**(Bullet ID),返回对应子弹对象的指针 + - 若**子弹编号**指向的子弹已经被删除或编号不合法,则返回 `nullptr` +- GetBullets + - 获取所有现存的子弹 +- GetParticle + - 根据输入的**粒子编号**(Particle ID),返回对应粒子对象的指针 + - 若**粒子编号**指向的粒子已经被删除或编号不合法,则返回 `nullptr` +- GetParticles + - 获取所有现存的粒子 + +### 事件队列 + +由于一个游戏帧在更新的过程中,不同对象的更新函数存在被调用的先后顺序。 +为了数据一致性,我们不希望先被调用的更新函数产生的影响立刻影响到后续被调用的更新函数, +例如:当两个单位可以同时击杀对方时,先被调用的对象如果立即对对方产生了伤害,那么后发单位直接死亡,会影响到游戏公平。 +因此,我们希望游戏对象的更新逻辑部分将可能影响到其他对象的事件延后结算,因此我们引入了事件队列机制: + +在所有游戏对象的更新函数调用完成后,事件队列会依次执行其中的内容。 +我们希望保证:事件与事件之间的相对顺序对计算结果没有干扰。 + +总而言之,这是一个保证游戏逻辑一致性的机制,已经提供的事件函数如下: + +```c++ +void PushEventMoveUnit(uint32_t unit_id, glm::vec2 new_position); +void PushEventRotateUnit(uint32_t unit_id, float new_rotation); +void PushEventDealDamage(uint32_t dst_unit_id, + uint32_t src_unit_id, + float damage); +void PushEventKillUnit(uint32_t dst_unit_id, uint32_t src_unit_id); +void PushEventRemoveObstacle(uint32_t obstacle_id); +void PushEventRemoveBullet(uint32_t bullet_id); +void PushEventRemoveParticle(uint32_t particle_id); +void PushEventRemoveUnit(uint32_t unit_id); + +template +void PushEventGenerateBullet(uint32_t unit_id, + uint32_t player_id, + glm::vec2 position, + float rotation = 0.0f, + float damage_scale = 1.0f, + Args... args); +``` + +- PushEventMoveUnit + - 压入一个单位移动事件 +- PushEventRotateUnit + - 压入一个单位旋转事件 +- PushEventDealDamage + - 压入一个伤害事件 +- PushEventKillUnit + - 压入一个击杀事件 + - 这种事件通常通过伤害事件产生 +- PushEventRemoveObstacle + - 压入一个障碍物移除事件 +- PushEventRemoveBullet + - 压入一个子弹移除事件 +- PushEventRemoveParticle + - 压入一个粒子移除事件 +- PushEventRemoveUnit + - 压入一个单位移除事件 + - 这种事件通常通过击杀事件产生 +- PushEventGenerateBullet + - 压入一个子弹生成事件 + - 和 Unit 类中的 GenerateBullet 函数相联系 + +### 随机变量 + +游戏中我们常常会见到随机机制,为了方便日后我们加入联机功能,我们需要使随机机制具有一致性。 +因此我们希望你编写的代码中所有涉及到的随机变量都通过以下函数获得: + +- RandomFloat + - 产生一个区间 [0, 1] 内均匀分布的随机浮点数 +- RandomInt + - 产生一个区间 [low_bound, high_bound] 内均匀分布的随机整数 +- RandomOnCircle + - 产生一个单位圆圆周上均匀分布的随机向量 +- RandomInCircle + - 产生一个单位圆内部均匀分布的随机向量 diff --git a/src/battle_game/core/bullet.cpp b/src/battle_game/core/bullet.cpp new file mode 100644 index 00000000..db40a716 --- /dev/null +++ b/src/battle_game/core/bullet.cpp @@ -0,0 +1,18 @@ +#include "battle_game/core/bullet.h" + +namespace battle_game { +Bullet::Bullet(GameCore *core, + uint32_t id, + uint32_t unit_id, + uint32_t player_id, + glm::vec2 position, + float rotation, + float damage_scale) + : Object(core, id, position, rotation), + unit_id_(unit_id), + player_id_(player_id), + damage_scale_(damage_scale) { +} + +Bullet::~Bullet() = default; +} // namespace battle_game diff --git a/src/battle_game/core/bullet.h b/src/battle_game/core/bullet.h new file mode 100644 index 00000000..9c628163 --- /dev/null +++ b/src/battle_game/core/bullet.h @@ -0,0 +1,24 @@ +#pragma once +#include "battle_game/core/object.h" +#include "battle_game/graphics/assets_manager.h" +#include "cstdint" + +namespace battle_game { +class GameCore; +class Bullet : public Object { + public: + Bullet(GameCore *core, + uint32_t id, + uint32_t unit_id, + uint32_t player_id, + glm::vec2 position, + float rotation, + float damage_scale); + ~Bullet() override; + + protected: + uint32_t unit_id_{}; + uint32_t player_id_{}; + float damage_scale_{1.0f}; +}; +} // namespace battle_game diff --git a/src/battle_game/core/bullets/bullets.h b/src/battle_game/core/bullets/bullets.h new file mode 100644 index 00000000..75a59f16 --- /dev/null +++ b/src/battle_game/core/bullets/bullets.h @@ -0,0 +1,2 @@ +#pragma once +#include "battle_game/core/bullets/cannon_ball.h" diff --git a/src/battle_game/core/bullets/cannon_ball.cpp b/src/battle_game/core/bullets/cannon_ball.cpp new file mode 100644 index 00000000..2ea94a5b --- /dev/null +++ b/src/battle_game/core/bullets/cannon_ball.cpp @@ -0,0 +1,56 @@ +#include "battle_game/core/bullets/cannon_ball.h" + +#include "battle_game/core/game_core.h" +#include "battle_game/core/particles/particles.h" + +namespace battle_game::bullet { +CannonBall::CannonBall(GameCore *core, + uint32_t id, + uint32_t unit_id, + uint32_t player_id, + glm::vec2 position, + float rotation, + float damage_scale, + glm::vec2 velocity) + : Bullet(core, id, unit_id, player_id, position, rotation, damage_scale), + velocity_(velocity) { +} + +void CannonBall::Render() { + SetTransformation(position_, rotation_, glm::vec2{0.1f}); + SetColor(game_core_->GetPlayerColor(player_id_)); + SetTexture(BATTLE_GAME_ASSETS_DIR "textures/particle3.png"); + DrawModel(0); +} + +void CannonBall::Update() { + position_ += velocity_ * kSecondPerTick; + bool should_die = false; + if (game_core_->IsBlockedByObstacles(position_)) { + should_die = true; + } + + auto &units = game_core_->GetUnits(); + for (auto &unit : units) { + if (unit.first == unit_id_) { + continue; + } + if (unit.second->IsHit(position_)) { + game_core_->PushEventDealDamage(unit.first, id_, damage_scale_ * 10.0f); + should_die = true; + } + } + + if (should_die) { + game_core_->PushEventRemoveBullet(id_); + } +} + +CannonBall::~CannonBall() { + for (int i = 0; i < 5; i++) { + game_core_->PushEventGenerateParticle( + position_, rotation_, game_core_->RandomInCircle() * 2.0f, 0.2f, + glm::vec4{0.0f, 0.0f, 0.0f, 1.0f}, 3.0f); + } +} +} // namespace battle_game::bullet diff --git a/src/battle_game/core/bullets/cannon_ball.h b/src/battle_game/core/bullets/cannon_ball.h new file mode 100644 index 00000000..e5f7e17b --- /dev/null +++ b/src/battle_game/core/bullets/cannon_ball.h @@ -0,0 +1,22 @@ +#pragma once +#include "battle_game/core/bullet.h" + +namespace battle_game::bullet { +class CannonBall : public Bullet { + public: + CannonBall(GameCore *core, + uint32_t id, + uint32_t unit_id, + uint32_t player_id, + glm::vec2 position, + float rotation, + float damage_scale, + glm::vec2 velocity); + ~CannonBall() override; + void Render() override; + void Update() override; + + private: + glm::vec2 velocity_{}; +}; +} // namespace battle_game::bullet diff --git a/src/battle_game/core/game_core.cpp b/src/battle_game/core/game_core.cpp new file mode 100644 index 00000000..53f1acb7 --- /dev/null +++ b/src/battle_game/core/game_core.cpp @@ -0,0 +1,323 @@ +#include "battle_game/core/game_core.h" + +namespace battle_game { + +GameCore::GameCore() { + auto mgr = AssetsManager::GetInstance(); + boundary_model_ = mgr->RegisterModel( + std::vector{ + {{-1.0f, 1.0f}, {0.0f, 0.0f}, {1.0f, 0.0f, 0.0f, 1.0f}}, + {{1.0f, 1.0f}, {0.0f, 0.0f}, {1.0f, 0.0f, 0.0f, 1.0f}}, + {{-1.0f, -1.0f}, {0.0f, 0.0f}, {1.0f, 0.0f, 0.0f, 0.0f}}, + {{1.0f, -1.0f}, {0.0f, 0.0f}, {1.0f, 0.0f, 0.0f, 0.0f}}}, + std::vector{0, 1, 2, 1, 2, 3}); + SetScene(); + GeneratePrimaryUnitList(); +} + +/* + * Update for 1 game tick. + * Order: obstacles, bullets, units, particles + * */ +void GameCore::Update() { + for (auto &player : players_) { + player.second->Update(); + } + + for (auto &obstacle : obstacles_) { + obstacle.second->Update(); + } + for (auto &bullet : bullets_) { + if (IsOutOfRange(bullet.second->GetPosition())) { + PushEventRemoveBullet(bullet.first); + continue; + } + bullet.second->Update(); + } + for (auto &units : units_) { + units.second->Update(); + } + for (auto &particle : particles_) { + if (IsOutOfRange(particle.second->GetPosition())) { + PushEventRemoveParticle(particle.first); + continue; + } + particle.second->Update(); + } + ProcessEventQueue(); +} + +/* + * Render the objects + * Order: obstacles, bullets, units, particles, life bars, helper + * */ +void GameCore::Render() { + auto observer = GetPlayer(render_perspective_); + if (observer) { + auto observing_unit = GetUnit(observer->GetPrimaryUnitId()); + if (observing_unit) { + SetCamera(observing_unit->GetPosition(), 0.0f); + } + } + + SetColor(); + SetTexture(); + SetTransformation( + glm::vec2{boundary_low_.x, (boundary_low_.y + boundary_high_.y) * 0.5f}, + glm::radians(-90.0f), + {(boundary_high_.y - boundary_low_.y) * 0.5f, 0.1f}); + DrawModel(boundary_model_); + SetTransformation( + glm::vec2{boundary_high_.x, (boundary_low_.y + boundary_high_.y) * 0.5f}, + glm::radians(90.0f), {(boundary_high_.y - boundary_low_.y) * 0.5f, 0.1f}); + DrawModel(boundary_model_); + SetTransformation( + glm::vec2{(boundary_low_.x + boundary_high_.x) * 0.5f, boundary_low_.y}, + glm::radians(0.0f), {(boundary_high_.x - boundary_low_.x) * 0.5f, 0.1f}); + DrawModel(boundary_model_); + SetTransformation( + glm::vec2{(boundary_low_.x + boundary_high_.x) * 0.5f, boundary_high_.y}, + glm::radians(180.0f), + {(boundary_high_.x - boundary_low_.x) * 0.5f, 0.1f}); + DrawModel(boundary_model_); + + for (auto &obstacle : obstacles_) { + obstacle.second->Render(); + } + for (auto &bullet : bullets_) { + bullet.second->Render(); + } + for (auto &units : units_) { + units.second->Render(); + } + for (auto &particle : particles_) { + particle.second->Render(); + } + for (auto &units : units_) { + units.second->RenderLifeBar(); + } + if (observer) { + auto observing_unit = GetUnit(observer->GetPrimaryUnitId()); + if (observing_unit) { + observing_unit->RenderHelper(); + } + } +} + +uint32_t GameCore::AddPlayer() { + uint32_t player_id = player_index_++; + players_[player_id] = std::make_unique(this, player_id); + return player_id; +} + +glm::vec4 GameCore::GetPlayerColor(uint32_t player_id) const { + if (render_perspective_ == 0) { + return glm::vec4{0.5f, 1.0f, 0.5f, 1.0f}; + } else if (render_perspective_ == player_id) { + return glm::vec4{1.0f, 1.0f, 1.0f, 1.0f}; + } else { + return glm::vec4{1.0f, 0.5f, 0.5f, 1.0f}; + } +} + +void GameCore::SetRenderPerspective(uint32_t player_id) { + render_perspective_ = player_id; +} + +uint32_t GameCore::GetRenderPerspective() const { + return render_perspective_; +} + +void GameCore::ProcessEventQueue() { + while (!event_queue_.empty()) { + event_queue_.front()(); + event_queue_.pop(); + } +} + +bool GameCore::IsBlockedByObstacles(glm::vec2 p) const { + if (IsOutOfRange(p)) { + return true; + } + for (auto &obstacle : obstacles_) { + if (obstacle.second->IsBlocked(p)) { + return true; + } + } + return false; +} + +Obstacle *GameCore::GetBlockedObstacle(glm::vec2 p) const { + if (!IsOutOfRange(p)) { + for (auto &obstacle : obstacles_) + if (obstacle.second->IsBlocked(p)) { + return obstacle.second.get(); + } + } + return nullptr; +} + +void GameCore::PushEventMoveUnit(uint32_t unit_id, glm::vec2 new_position) { + event_queue_.emplace([this, unit_id, new_position]() { + auto unit = GetUnit(unit_id); + if (unit) { + unit->SetPosition(new_position); + } + }); +} + +void GameCore::PushEventRotateUnit(uint32_t unit_id, float new_rotation) { + event_queue_.emplace([this, unit_id, new_rotation]() { + auto unit = GetUnit(unit_id); + if (unit) { + unit->SetRotation(new_rotation); + } + }); +} + +Unit *GameCore::GetUnit(uint32_t unit_id) const { + if (!units_.count(unit_id)) { + return nullptr; + } + return units_.at(unit_id).get(); +} + +Bullet *GameCore::GetBullet(uint32_t bullet_id) const { + if (!bullets_.count(bullet_id)) { + return nullptr; + } + return bullets_.at(bullet_id).get(); +} + +Particle *GameCore::GetParticle(uint32_t particle_id) const { + if (!particles_.count(particle_id)) { + return nullptr; + } + return particles_.at(particle_id).get(); +} + +Obstacle *GameCore::GetObstacle(uint32_t obstacle_id) const { + if (!obstacles_.count(obstacle_id)) { + return nullptr; + } + return obstacles_.at(obstacle_id).get(); +} + +Player *GameCore::GetPlayer(uint32_t player_id) const { + if (!players_.count(player_id)) { + return nullptr; + } + return players_.at(player_id).get(); +} + +void GameCore::SetCamera(glm::vec2 position, float rotation) { + camera_position_ = position; + camera_rotation_ = rotation; +} + +void GameCore::PushEventDealDamage(uint32_t dst_unit_id, + uint32_t src_unit_id, + float damage) { + event_queue_.emplace([=]() { + auto unit = GetUnit(dst_unit_id); + if (unit) { + unit->SetHealth(unit->GetHealth() - damage / unit->GetMaxHealth()); + if (unit->GetHealth() <= 0.0f) { + PushEventKillUnit(dst_unit_id, src_unit_id); + } + } + }); +} + +void GameCore::PushEventRemoveObstacle(uint32_t obstacle_id) { + event_queue_.emplace([=]() { + if (obstacles_.count(obstacle_id)) { + obstacles_.erase(obstacle_id); + } + }); +} + +void GameCore::PushEventRemoveBullet(uint32_t bullet_id) { + event_queue_.emplace([=]() { + if (bullets_.count(bullet_id)) { + bullets_.erase(bullet_id); + } + }); +} + +void GameCore::PushEventRemoveParticle(uint32_t particle_id) { + event_queue_.emplace([=]() { + if (particles_.count(particle_id)) { + particles_.erase(particle_id); + } + }); +} + +void GameCore::PushEventRemoveUnit(uint32_t unit_id) { + event_queue_.emplace([=]() { + if (units_.count(unit_id)) { + units_.erase(unit_id); + } + }); +} + +void GameCore::PushEventKillUnit(uint32_t dst_unit_id, uint32_t src_unit_id) { + event_queue_.emplace([=]() { PushEventRemoveUnit(dst_unit_id); }); +} + +float GameCore::RandomFloat() { + return std::uniform_real_distribution()(random_device_); +} + +int GameCore::RandomInt(int low_bound, int high_bound) { + return std::uniform_int_distribution(low_bound, + high_bound)(random_device_); +} + +void GameCore::SetScene() { + AddObstacle(glm::vec2{-3.0f, 4.0f}); + respawn_points_.emplace_back(glm::vec2{0.0f}, 0.0f); + respawn_points_.emplace_back(glm::vec2{3.0f, 4.0f}, glm::radians(90.0f)); + boundary_low_ = {-10.0f, -10.0f}; + boundary_high_ = {10.0f, 10.0f}; +} + +uint32_t GameCore::AllocatePrimaryUnit(uint32_t player_id) { + auto player = GetPlayer(player_id); + if (!player) { + return 0; + } + auto unit_id = + primary_unit_allocation_functions_[player->SelectedUnit()](player_id); + auto unit = GetUnit(unit_id); + auto respawn_point = + respawn_points_[RandomInt(0, int(respawn_points_.size()) - 1)]; + unit->SetPosition(respawn_point.first); + unit->SetRotation(respawn_point.second); + return unit_id; +} + +glm::vec2 GameCore::RandomOnCircle() { + auto theta = RandomFloat() * glm::pi() * 2.0f; + return {std::sin(theta), std::cos(theta)}; +} + +glm::vec2 GameCore::RandomInCircle() { + auto theta = RandomFloat() * glm::pi() * 2.0f; + auto length = std::sqrt(RandomFloat()); + return {std::sin(theta) * length, std::cos(theta) * length}; +} + +bool GameCore::IsOutOfRange(glm::vec2 p) const { + return p.x < boundary_low_.x || p.x > boundary_high_.x || + p.y < boundary_low_.y || p.y > boundary_high_.y; +} + +std::vector GameCore::GetSelectableUnitList() const { + std::vector result; + for (auto &selectable_unit : selectable_unit_list_) { + result.emplace_back(selectable_unit.data()); + } + return result; +} +} // namespace battle_game diff --git a/src/battle_game/core/game_core.h b/src/battle_game/core/game_core.h new file mode 100644 index 00000000..0e81d8e4 --- /dev/null +++ b/src/battle_game/core/game_core.h @@ -0,0 +1,229 @@ +#pragma once +#include "battle_game/core/bullet.h" +#include "battle_game/core/bullets/bullets.h" +#include "battle_game/core/input_data.h" +#include "battle_game/core/obstacle.h" +#include "battle_game/core/obstacles/obstacles.h" +#include "battle_game/core/particle.h" +#include "battle_game/core/particles/particles.h" +#include "battle_game/core/player.h" +#include "battle_game/core/unit.h" +#include "battle_game/core/units/units.h" +#include "battle_game/graphics/graphics.h" +#include "functional" +#include "grassland/grassland.h" +#include "map" +#include "queue" +#include "random" +#include "vector" + +namespace battle_game { +constexpr int kTickPerSecond = 60; +constexpr float kSecondPerTick = 1.0f / float(kTickPerSecond); +class GameCore { + public: + GameCore(); + + void SetScene(); + + template + void AddPrimaryUnitAllocationFunction(Args... args); + + void GeneratePrimaryUnitList(); + uint32_t AllocatePrimaryUnit(uint32_t player_id); + [[nodiscard]] std::vector GetSelectableUnitList() const; + [[nodiscard]] const std::vector &GetSelectableUnitListSkill() const { + return selectable_unit_list_skill_; + } + + void Update(); + void Render(); + + template + uint32_t AddUnit(uint32_t player_id, Args... args) { + auto unit_index = unit_index_++; + units_[unit_index] = + std::make_unique(this, unit_index, player_id, args...); + return unit_index; + } + + template + uint32_t AddObstacle(glm::vec2 position, + float rotation = 0.0f, + Args... args) { + auto obstacle_index = obstacle_index_++; + obstacles_[obstacle_index] = std::make_unique( + this, obstacle_index, position, rotation, args...); + return obstacle_index; + } + + template + uint32_t AddBullet(uint32_t unit_id, + uint32_t player_id, + glm::vec2 position, + float rotation = 0.0f, + float damage_scale = 1.0f, + Args... args) { + if (IsOutOfRange(position)) { + return 0; + } + auto bullet_index = bullet_index_++; + bullets_[bullet_index] = + std::make_unique(this, bullet_index, unit_id, player_id, + position, rotation, damage_scale, args...); + return bullet_index; + } + template + uint32_t AddParticle(glm::vec2 position, + float rotation = 0.0f, + Args... args) { + if (IsOutOfRange(position)) { + return 0; + } + auto particle_index = particle_index_++; + particles_[particle_index] = std::make_unique( + this, particle_index, position, rotation, args...); + return particle_index; + } + + uint32_t AddPlayer(); + + [[nodiscard]] Unit *GetUnit(uint32_t unit_id) const; + [[nodiscard]] Bullet *GetBullet(uint32_t bullet_id) const; + [[nodiscard]] Particle *GetParticle(uint32_t particle_id) const; + [[nodiscard]] Obstacle *GetObstacle(uint32_t obstacle_id) const; + [[nodiscard]] Player *GetPlayer(uint32_t player_id) const; + [[nodiscard]] const std::map> &GetUnits() + const { + return units_; + } + [[nodiscard]] const std::map> &GetBullets() + const { + return bullets_; + } + [[nodiscard]] const std::map> + &GetParticles() const { + return particles_; + } + [[nodiscard]] const std::map> + &GetObstacles() const { + return obstacles_; + } + [[nodiscard]] const std::map> &GetPlayers() + const { + return players_; + } + + void SetRenderPerspective(uint32_t player_id); + [[nodiscard]] uint32_t GetRenderPerspective() const; + [[nodiscard]] glm::vec4 GetPlayerColor(uint32_t player_id) const; + + [[nodiscard]] bool IsOutOfRange(glm::vec2 p) const; + [[nodiscard]] bool IsBlockedByObstacles(glm::vec2 p) const; + [[nodiscard]] Obstacle *GetBlockedObstacle(glm::vec2 p) const; + + void PushEventMoveUnit(uint32_t unit_id, glm::vec2 new_position); + void PushEventRotateUnit(uint32_t unit_id, float new_rotation); + void PushEventDealDamage(uint32_t dst_unit_id, + uint32_t src_unit_id, + float damage); + void PushEventKillUnit(uint32_t dst_unit_id, uint32_t src_unit_id); + void PushEventRemoveObstacle(uint32_t obstacle_id); + void PushEventRemoveBullet(uint32_t bullet_id); + void PushEventRemoveParticle(uint32_t particle_id); + void PushEventRemoveUnit(uint32_t unit_id); + + template + void PushEventGenerateBullet(uint32_t unit_id, + uint32_t player_id, + glm::vec2 position, + float rotation = 0.0f, + float damage_scale = 1.0f, + Args... args) { + event_queue_.emplace([=]() { + AddBullet(unit_id, player_id, position, rotation, + damage_scale, args...); + }); + } + + template + void PushEventGenerateObstacle(glm::vec2 position, + float rotation = 0.0f, + Args... args) { + event_queue_.emplace( + [=]() { AddObstacle(position, rotation, args...); }); + } + + template + void PushEventGenerateParticle(glm::vec2 position, + float rotation = 0.0f, + Args... args) { + event_queue_.emplace( + [=]() { AddParticle(position, rotation, args...); }); + } + + void ProcessEventQueue(); + + void SetCamera(glm::vec2 position, float rotation = 0.0f); + [[nodiscard]] glm::vec2 GetCameraPosition() const { + return camera_position_; + } + [[nodiscard]] float GetCameraRotation() const { + return camera_rotation_; + } + + /* + * Return a uniform random real number in range [0, 1] + * */ + float RandomFloat(); + + /* + * Return a uniform random integer number in range [low_bound, high_bound] + * */ + int RandomInt(int low_bound, int high_bound); + glm::vec2 RandomOnCircle(); + glm::vec2 RandomInCircle(); + + private: + std::queue> event_queue_; + + std::map> units_; + uint32_t unit_index_{1}; + std::map> bullets_; + uint32_t bullet_index_{1}; + std::map> particles_; + uint32_t particle_index_{1}; + std::map> obstacles_; + uint32_t obstacle_index_{1}; + std::map> players_; + uint32_t player_index_{1}; + + uint32_t render_perspective_{ + 0}; // This is a player id, defines which player is currently watching + // the scene. 0 denote neutral. + + glm::vec2 camera_position_{0.0f}; + float camera_rotation_{0.0f}; + + glm::vec2 boundary_low_{-10.0f, -10.0f}; + glm::vec2 boundary_high_{10.0f, 10.0f}; + uint32_t boundary_model_{}; + + std::mt19937 random_device_{0}; + + std::vector> respawn_points_; + std::vector> + primary_unit_allocation_functions_; + std::vector selectable_unit_list_; + std::vector selectable_unit_list_skill_; +}; + +template +void Unit::GenerateBullet(glm::vec2 position, + float rotation, + float damage_scale, + Args... args) { + game_core_->PushEventGenerateBullet( + id_, player_id_, position, rotation, damage_scale, args...); +} +} // namespace battle_game diff --git a/src/battle_game/core/input_data.cpp b/src/battle_game/core/input_data.cpp new file mode 100644 index 00000000..55a799d7 --- /dev/null +++ b/src/battle_game/core/input_data.cpp @@ -0,0 +1,3 @@ +#include "battle_game/core/input_data.h" + +namespace battle_game {} diff --git a/src/battle_game/core/input_data.h b/src/battle_game/core/input_data.h new file mode 100644 index 00000000..a2e107c1 --- /dev/null +++ b/src/battle_game/core/input_data.h @@ -0,0 +1,14 @@ +#pragma once +#include "GLFW/glfw3.h" +#include "glm/glm.hpp" + +namespace battle_game { +constexpr int kKeyRange = GLFW_KEY_LAST + 1; +constexpr int kMouseButtonRange = GLFW_MOUSE_BUTTON_LAST + 1; +struct InputData { + bool key_down[kKeyRange]{}; + bool mouse_button_down[kMouseButtonRange]{}; + bool mouse_button_clicked[kMouseButtonRange]{}; + glm::vec2 mouse_cursor_position{0.0f}; +}; +} // namespace battle_game diff --git a/src/battle_game/core/object.cpp b/src/battle_game/core/object.cpp new file mode 100644 index 00000000..87123cb2 --- /dev/null +++ b/src/battle_game/core/object.cpp @@ -0,0 +1,32 @@ +#include "battle_game/core/object.h" + +#include "glm/gtc/matrix_transform.hpp" + +namespace battle_game { +glm::vec2 Object::LocalToWorld(const glm::vec2 p) const { + return glm::vec2{ + glm::translate(glm::mat4{1.0f}, glm::vec3{position_, 0.0f}) * + glm::rotate(glm::mat4{1.0f}, rotation_, glm::vec3{0.0f, 0.0f, 1.0f}) * + glm::vec4{p, 0.0f, 1.0f}}; +} + +glm::vec2 Object::WorldToLocal(const glm::vec2 p) const { + return glm::vec2{ + glm::inverse(glm::translate(glm::mat4{1.0f}, glm::vec3{position_, 0.0f}) * + glm::rotate(glm::mat4{1.0f}, rotation_, + glm::vec3{0.0f, 0.0f, 1.0f})) * + glm::vec4{p, 0.0f, 1.0f}}; +} + +Object::Object(GameCore *game_core, + uint32_t id, + glm::vec2 position, + float rotation) { + game_core_ = game_core; + position_ = position; + rotation_ = rotation; + id_ = id; +} + +Object::~Object() = default; +} // namespace battle_game diff --git a/src/battle_game/core/object.h b/src/battle_game/core/object.h new file mode 100644 index 00000000..46f651cf --- /dev/null +++ b/src/battle_game/core/object.h @@ -0,0 +1,43 @@ +#pragma once +#include "battle_game/graphics/graphics.h" +#include "glm/glm.hpp" +#define SKILL_ADD_FUNCTION(function_) std::bind(&function_, this) +#define SKILL_SWITCH_BULLET(function_) \ + std::bind(&function_, this, std::placeholders::_1) +#define SKILL_ADD_NULL nullptr + +namespace battle_game { +class GameCore; + +class Object { + public: + Object(GameCore *game_core, + uint32_t id, + glm::vec2 position = glm::vec2{0.0f}, + float rotation = 0.0f); + virtual ~Object(); + [[nodiscard]] glm::vec2 LocalToWorld(glm::vec2 p) const; + [[nodiscard]] glm::vec2 WorldToLocal(glm::vec2 p) const; + [[nodiscard]] glm::vec2 GetPosition() const { + return position_; + } + [[nodiscard]] float GetRotation() const { + return rotation_; + } + [[nodiscard]] GameCore *GetGameCore() const { + return game_core_; + } + [[nodiscard]] uint32_t GetId() const { + return id_; + } + + virtual void Render() = 0; + virtual void Update() = 0; + + protected: + GameCore *game_core_{nullptr}; + glm::vec2 position_{0.0f}; // offset from the origin (0, 0) + float rotation_{0.0f}; // angle in radians + uint32_t id_{0}; +}; +} // namespace battle_game diff --git a/src/battle_game/core/obstacle.cpp b/src/battle_game/core/obstacle.cpp new file mode 100644 index 00000000..f756b7e2 --- /dev/null +++ b/src/battle_game/core/obstacle.cpp @@ -0,0 +1,17 @@ +#include "battle_game/core/obstacle.h" + +namespace battle_game { + +Obstacle::Obstacle(GameCore *game_core, + uint32_t id, + glm::vec2 position, + float rotation) + : Object(game_core, id, position, rotation) { +} + +void Obstacle::Update() { +} + +void Obstacle::Render() { +} +} // namespace battle_game diff --git a/src/battle_game/core/obstacle.h b/src/battle_game/core/obstacle.h new file mode 100644 index 00000000..9df74e3a --- /dev/null +++ b/src/battle_game/core/obstacle.h @@ -0,0 +1,22 @@ +#pragma once +#include "battle_game/core/object.h" +#include "glm/glm.hpp" + +namespace battle_game { +class Obstacle : public Object { + public: + Obstacle(GameCore *game_core, + uint32_t id, + glm::vec2 position, + float rotation = 0.0f); + [[nodiscard]] virtual bool IsBlocked(glm::vec2 p) const = 0; + void Update() override; + void Render() override; + virtual std::pair GetSurfaceNormal(glm::vec2 origin, + glm::vec2 terminus) { + return std::make_pair(glm::vec2(0, 0), glm::vec2(0, 0)); + } + + protected: +}; +} // namespace battle_game diff --git a/src/battle_game/core/obstacles/block.cpp b/src/battle_game/core/obstacles/block.cpp new file mode 100644 index 00000000..bf8c3d25 --- /dev/null +++ b/src/battle_game/core/obstacles/block.cpp @@ -0,0 +1,25 @@ +#include "battle_game/core/obstacles/block.h" + +namespace battle_game::obstacle { + +Block::Block(GameCore *game_core, + uint32_t id, + glm::vec2 position, + float rotation, + glm::vec2 scale) + : Obstacle(game_core, id, position, rotation) { +} + +bool Block::IsBlocked(glm::vec2 p) const { + p = WorldToLocal(p); + return p.x <= scale_.x && p.x >= -scale_.x && p.y <= scale_.y && + p.y >= -scale_.y; +} + +void Block::Render() { + battle_game::SetColor(glm::vec4{0.0f, 0.0f, 0.0f, 1.0f}); + battle_game::SetTexture(0); + battle_game::SetTransformation(position_, rotation_, scale_); + battle_game::DrawModel(0); +} +} // namespace battle_game::obstacle diff --git a/src/battle_game/core/obstacles/block.h b/src/battle_game/core/obstacles/block.h new file mode 100644 index 00000000..56440ab7 --- /dev/null +++ b/src/battle_game/core/obstacles/block.h @@ -0,0 +1,18 @@ +#pragma once +#include "battle_game/core/obstacle.h" + +namespace battle_game::obstacle { +class Block : public Obstacle { + public: + Block(GameCore *game_core, + uint32_t id, + glm::vec2 position, + float rotation = 0.0f, + glm::vec2 scale = glm::vec2{1.0f, 1.0f}); + + private: + [[nodiscard]] bool IsBlocked(glm::vec2 p) const override; + void Render() override; + glm::vec2 scale_{1.0f}; +}; +} // namespace battle_game::obstacle diff --git a/src/battle_game/core/obstacles/obstacles.h b/src/battle_game/core/obstacles/obstacles.h new file mode 100644 index 00000000..3d9b409d --- /dev/null +++ b/src/battle_game/core/obstacles/obstacles.h @@ -0,0 +1,3 @@ +#pragma once + +#include "battle_game/core/obstacles/block.h" diff --git a/src/battle_game/core/particle.cpp b/src/battle_game/core/particle.cpp new file mode 100644 index 00000000..a8422c6d --- /dev/null +++ b/src/battle_game/core/particle.cpp @@ -0,0 +1,10 @@ +#include "battle_game/core/particle.h" + +namespace battle_game { +Particle::Particle(GameCore *game_core, + uint32_t id, + glm::vec2 position, + float rotation) + : Object(game_core, id, position, rotation) { +} +} // namespace battle_game diff --git a/src/battle_game/core/particle.h b/src/battle_game/core/particle.h new file mode 100644 index 00000000..a3b59a80 --- /dev/null +++ b/src/battle_game/core/particle.h @@ -0,0 +1,14 @@ +#pragma once +#include "battle_game/core/object.h" + +namespace battle_game { +class Particle : public Object { + public: + Particle(GameCore *game_core, + uint32_t id, + glm::vec2 position, + float rotation = 0.0f); + + private: +}; +} // namespace battle_game diff --git a/src/battle_game/core/particles/particles.h b/src/battle_game/core/particles/particles.h new file mode 100644 index 00000000..4e23cb9e --- /dev/null +++ b/src/battle_game/core/particles/particles.h @@ -0,0 +1,2 @@ +#pragma once +#include "battle_game/core/particles/smoke.h" diff --git a/src/battle_game/core/particles/smoke.cpp b/src/battle_game/core/particles/smoke.cpp new file mode 100644 index 00000000..9660488a --- /dev/null +++ b/src/battle_game/core/particles/smoke.cpp @@ -0,0 +1,35 @@ +#include "battle_game/core/particles/smoke.h" + +#include "battle_game/core/game_core.h" + +namespace battle_game::particle { +Smoke::Smoke(GameCore *game_core, + uint32_t id, + glm::vec2 position, + float rotation, + glm::vec2 v, + float size, + glm::vec4 color, + float decay_scale) + : Particle(game_core, id, position, rotation), + v_(v), + size_(size), + color_(color), + decay_scale_(decay_scale) { +} + +void Smoke::Render() { + SetTransformation(position_, rotation_, glm::vec2{size_}); + SetColor(glm::vec4{glm::vec3{1.0f}, strength_} * color_); + SetTexture(BATTLE_GAME_ASSETS_DIR "textures/particle2.png"); + DrawModel(); +} + +void Smoke::Update() { + position_ += v_ * kSecondPerTick; + strength_ -= kSecondPerTick * decay_scale_; + if (strength_ < 0.0f) { + game_core_->PushEventRemoveParticle(id_); + } +} +} // namespace battle_game::particle diff --git a/src/battle_game/core/particles/smoke.h b/src/battle_game/core/particles/smoke.h new file mode 100644 index 00000000..a7623932 --- /dev/null +++ b/src/battle_game/core/particles/smoke.h @@ -0,0 +1,25 @@ +#pragma once +#include "battle_game/core/particle.h" + +namespace battle_game::particle { +class Smoke : public Particle { + public: + Smoke(GameCore *game_core, + uint32_t id, + glm::vec2 position, + float rotation, + glm::vec2 v, + float size = 0.2f, + glm::vec4 color = glm::vec4{1.0f}, + float decay_scale = 1.0f); + void Render() override; + void Update() override; + + private: + glm::vec2 v_{}; + float strength_{1.0f}; + float size_{}; + float decay_scale_{}; + glm::vec4 color_{}; +}; +} // namespace battle_game::particle diff --git a/src/battle_game/core/player.cpp b/src/battle_game/core/player.cpp new file mode 100644 index 00000000..545fa34d --- /dev/null +++ b/src/battle_game/core/player.cpp @@ -0,0 +1,22 @@ +#include "battle_game/core/player.h" + +#include "battle_game/core/game_core.h" + +namespace battle_game { +Player::Player(GameCore *game_core, uint32_t id) + : game_core_(game_core), id_(id) { +} + +void Player::Update() { + auto primary_unit = game_core_->GetUnit(primary_unit_id_); + if (!primary_unit) { + if (!resurrection_count_down_) { + resurrection_count_down_ = kTickPerSecond * 5; // Respawn after 5 seconds + } + resurrection_count_down_--; + if (!resurrection_count_down_) { + primary_unit_id_ = game_core_->AllocatePrimaryUnit(id_); + } + } +} +} // namespace battle_game diff --git a/src/battle_game/core/player.h b/src/battle_game/core/player.h new file mode 100644 index 00000000..355b42ea --- /dev/null +++ b/src/battle_game/core/player.h @@ -0,0 +1,39 @@ +#pragma once +#include "cstdint" +#include "input_data.h" + +namespace battle_game { +class GameCore; +class Player { + public: + Player(GameCore *game_core, uint32_t id); + [[nodiscard]] uint32_t GetId() const { + return id_; + } + void SetInputData(const InputData &input_data) { + input_data_ = input_data; + } + [[nodiscard]] const InputData &GetInputData() const { + return input_data_; + } + [[nodiscard]] uint32_t GetPrimaryUnitId() const { + return primary_unit_id_; + } + int &SelectedUnit() { + return selected_unit_; + } + + void Update(); + [[nodiscard]] uint32_t GetResurrectionCountDown() const { + return resurrection_count_down_; + } + + private: + GameCore *game_core_{}; + uint32_t id_{}; + InputData input_data_{}; + uint32_t primary_unit_id_{}; + uint32_t resurrection_count_down_{1}; + int selected_unit_{0}; +}; +} // namespace battle_game diff --git a/src/battle_game/core/selectable_units.cpp b/src/battle_game/core/selectable_units.cpp new file mode 100644 index 00000000..ce8abbcc --- /dev/null +++ b/src/battle_game/core/selectable_units.cpp @@ -0,0 +1,36 @@ +#include "battle_game/core/game_core.h" + +namespace battle_game { + +template +void GameCore::AddPrimaryUnitAllocationFunction(Args... args) { + primary_unit_allocation_functions_.emplace_back([=](uint32_t player_id) { + return AddUnit(player_id, args...); + }); +} + +void GameCore::GeneratePrimaryUnitList() { + std::unique_ptr unit; + +#define ADD_SELECTABLE_UNIT(UnitType) \ + unit = std::make_unique(nullptr, 0, 0); \ + AddPrimaryUnitAllocationFunction(); \ + selectable_unit_list_.push_back(unit->UnitName() + std::string(" - By ") + \ + unit->Author()); \ + selectable_unit_list_skill_.push_back(true); + +#define ADD_SELECTABLE_UNIT_WITHOUT_SKILL(UnitType) \ + unit = std::make_unique(nullptr, 0, 0); \ + AddPrimaryUnitAllocationFunction(); \ + selectable_unit_list_.push_back(unit->UnitName() + std::string(" - By ") + \ + unit->Author()); \ + selectable_unit_list_skill_.push_back(false); + + /* + * TODO: Add Your Unit Here! + * */ + ADD_SELECTABLE_UNIT(unit::Tank); + + unit.reset(); +} +} // namespace battle_game diff --git a/src/battle_game/core/unit.cpp b/src/battle_game/core/unit.cpp new file mode 100644 index 00000000..1c64960c --- /dev/null +++ b/src/battle_game/core/unit.cpp @@ -0,0 +1,127 @@ +#include "battle_game/core/unit.h" + +#include "battle_game/core/game_core.h" + +namespace battle_game { + +namespace { +uint32_t life_bar_model_index = 0xffffffffu; +} // namespace + +Unit::Unit(GameCore *game_core, uint32_t id, uint32_t player_id) + : Object(game_core, id) { + player_id_ = player_id; + lifebar_offset_ = {0.0f, 1.0f}; + background_lifebar_color_ = {1.0f, 0.0f, 0.0f, 0.9f}; + front_lifebar_color_ = {0.0f, 1.0f, 0.0f, 0.9f}; + fadeout_lifebar_color_ = {1.0f, 1.0f, 1.0f, 0.5f}; + fadeout_health_ = 1; + if (!~life_bar_model_index) { + auto mgr = AssetsManager::GetInstance(); + life_bar_model_index = mgr->RegisterModel( + {{{-0.5f, 0.08f}, {0.0f, 0.0f}, {1.0f, 1.0f, 1.0f, 1.0f}}, + {{-0.5f, -0.08f}, {0.0f, 0.0f}, {1.0f, 1.0f, 1.0f, 1.0f}}, + {{0.5f, 0.08f}, {0.0f, 0.0f}, {1.0f, 1.0f, 1.0f, 1.0f}}, + {{0.5f, -0.08f}, {0.0f, 0.0f}, {1.0f, 1.0f, 1.0f, 1.0f}}}, + {0, 1, 2, 1, 2, 3}); + } +} + +void Unit::SetPosition(glm::vec2 position) { + position_ = position; +} + +void Unit::SetRotation(float rotation) { + rotation_ = rotation; +} + +float Unit::GetSpeedScale() const { + return 1.0f; +} + +float Unit::GetDamageScale() const { + return 1.0f; +} + +float Unit::BasicMaxHealth() const { + return 100.0f; +} + +float Unit::GetHealthScale() const { + return 1.0f; +} + +void Unit::SetLifeBarLength(float new_length) { + lifebar_length_ = std::min(new_length, 0.0f); +} +void Unit::SetLifeBarOffset(glm::vec2 new_offset) { + lifebar_offset_ = new_offset; +} +void Unit::SetLifeBarFrontColor(glm::vec4 new_color) { + front_lifebar_color_ = new_color; +} +void Unit::SetLifeBarBackgroundColor(glm::vec4 new_color) { + background_lifebar_color_ = new_color; +} +void Unit::SetLifeBarFadeoutColor(glm::vec4 new_color) { + fadeout_lifebar_color_ = new_color; +} +float Unit::GetLifeBarLength() { + return lifebar_length_; +} +glm::vec2 Unit::GetLifeBarOffset() { + return lifebar_offset_; +} +glm::vec4 Unit::GetLifeBarFrontColor() { + return front_lifebar_color_; +} +glm::vec4 Unit::GetLifeBarBackgroundColor() { + return background_lifebar_color_; +} +glm::vec4 Unit::GetLifeBarFadeoutColor() { + return fadeout_lifebar_color_; +} + +void Unit::ShowLifeBar() { + lifebar_display_ = true; +} +void Unit::HideLifeBar() { + lifebar_display_ = false; +} + +void Unit::RenderLifeBar() { + if (lifebar_display_) { + auto parent_unit = game_core_->GetUnit(id_); + auto pos = parent_unit->GetPosition() + lifebar_offset_; + auto health = parent_unit->GetHealth(); + SetTransformation(pos, 0.0f, {lifebar_length_, 1.0f}); + SetColor(background_lifebar_color_); + SetTexture(0); + DrawModel(life_bar_model_index); + glm::vec2 shift = {(float)lifebar_length_ * (1 - health) / 2, 0.0f}; + SetTransformation(pos - shift, 0.0f, {lifebar_length_ * health, 1.0f}); + SetColor(front_lifebar_color_); + DrawModel(life_bar_model_index); + if (std::fabs(health - fadeout_health_) >= 0.01f) { + fadeout_health_ = health + (fadeout_health_ - health) * 0.93; + shift = {lifebar_length_ * (health + fadeout_health_ - 1) / 2, 0.0f}; + SetTransformation(pos + shift, 0.0f, + {lifebar_length_ * (health - fadeout_health_), 1.0f}); + SetColor(fadeout_lifebar_color_); + DrawModel(life_bar_model_index); + } else { + fadeout_health_ = health; + } + } +} + +void Unit::RenderHelper() { +} + +const char *Unit::UnitName() const { + return "Unknown Unit"; +} +const char *Unit::Author() const { + return "Unknown Author"; +} +} // namespace battle_game diff --git a/src/battle_game/core/unit.h b/src/battle_game/core/unit.h new file mode 100644 index 00000000..4400a85d --- /dev/null +++ b/src/battle_game/core/unit.h @@ -0,0 +1,100 @@ +#pragma once +#include "battle_game/core/object.h" +#include "glm/glm.hpp" + +namespace battle_game { + +class Bullet; + +class Unit : public Object { + public: + Unit(GameCore *game_core, uint32_t id, uint32_t player_id); + + uint32_t &GetPlayerId() { + return player_id_; + } + [[nodiscard]] uint32_t GetPlayerId() const { + return player_id_; + } + void SetPosition(glm::vec2 position); + void SetRotation(float rotation); + + [[nodiscard]] virtual float GetDamageScale() const; + [[nodiscard]] virtual float GetSpeedScale() const; + [[nodiscard]] virtual float BasicMaxHealth() const; + [[nodiscard]] virtual float GetHealthScale() const; + [[nodiscard]] virtual float GetMaxHealth() const { + return std::max(GetHealthScale() * BasicMaxHealth(), 1.0f); + } + + /* + * Health value is in range [0, 1], represents the remaining health in ratio + * form. GetHealth() * GetMaxHealth() represent true remaining health of the + * unit. + * */ + [[nodiscard]] float GetHealth() const { + return health_; + } + + /* + * The value of new_health will be clamped to [0, 1] + * */ + void SetHealth(float new_health) { + health_ = std::clamp(new_health, 0.0f, 1.0f); + } + + void SetLifeBarLength(float new_length); + void SetLifeBarOffset(glm::vec2 new_offset); + void SetLifeBarFrontColor(glm::vec4 new_color); + void SetLifeBarBackgroundColor(glm::vec4 new_color); + void SetLifeBarFadeoutColor(glm::vec4 new_color); + [[nodiscard]] float GetLifeBarLength(); + [[nodiscard]] glm::vec2 GetLifeBarOffset(); + [[nodiscard]] glm::vec4 GetLifeBarFrontColor(); + [[nodiscard]] glm::vec4 GetLifeBarBackgroundColor(); + [[nodiscard]] glm::vec4 GetLifeBarFadeoutColor(); + + void ShowLifeBar(); + void HideLifeBar(); + virtual void RenderLifeBar(); + + /* + * This virtual function is used to render some extra helpers, such as + * predicted trajectory of the bullet the unit will shoot, and etc., only + * in the first-person perspective. + * */ + virtual void RenderHelper(); + + /* + * This virtual function is used to check whether a bullet at the position + * have hit the unit. If the position is inside the unit area, then return + * true, otherwise return false. + * */ + [[nodiscard]] virtual bool IsHit(glm::vec2 position) const = 0; + + template + void GenerateBullet(glm::vec2 position, + float rotation, + float damage_scale = 1.0f, + Args... args); + + [[nodiscard]] virtual const char *UnitName() const; + [[nodiscard]] virtual const char *Author() const; + + protected: + uint32_t player_id_{}; + float health_{1.0f}; + bool lifebar_display_{true}; + glm::vec2 lifebar_offset_{}; + float lifebar_length_{2.4f}; + glm::vec4 front_lifebar_color_{}; + glm::vec4 background_lifebar_color_{}; + glm::vec4 fadeout_lifebar_color_{}; + + private: + float fadeout_health_; +}; + +} // namespace battle_game + +// add something to pull diff --git a/src/battle_game/core/units/tiny_tank.cpp b/src/battle_game/core/units/tiny_tank.cpp new file mode 100644 index 00000000..fc65eebf --- /dev/null +++ b/src/battle_game/core/units/tiny_tank.cpp @@ -0,0 +1,166 @@ +#include "tiny_tank.h" + +#include "battle_game/core/bullets/bullets.h" +#include "battle_game/core/game_core.h" +#include "battle_game/graphics/graphics.h" + +namespace battle_game::unit { + +namespace { +uint32_t tank_body_model_index = 0xffffffffu; +uint32_t tank_turret_model_index = 0xffffffffu; +} // namespace + +Tank::Tank(GameCore *game_core, uint32_t id, uint32_t player_id) + : Unit(game_core, id, player_id) { + if (!~tank_body_model_index) { + auto mgr = AssetsManager::GetInstance(); + { + /* Tank Body */ + tank_body_model_index = mgr->RegisterModel( + { + {{-0.8f, 0.8f}, {0.0f, 0.0f}, {1.0f, 1.0f, 1.0f, 1.0f}}, + {{-0.8f, -1.0f}, {0.0f, 0.0f}, {1.0f, 1.0f, 1.0f, 1.0f}}, + {{0.8f, 0.8f}, {0.0f, 0.0f}, {1.0f, 1.0f, 1.0f, 1.0f}}, + {{0.8f, -1.0f}, {0.0f, 0.0f}, {1.0f, 1.0f, 1.0f, 1.0f}}, + // distinguish front and back + {{0.6f, 1.0f}, {0.0f, 0.0f}, {1.0f, 1.0f, 1.0f, 1.0f}}, + {{-0.6f, 1.0f}, {0.0f, 0.0f}, {1.0f, 1.0f, 1.0f, 1.0f}}, + }, + {0, 1, 2, 1, 2, 3, 0, 2, 5, 2, 4, 5}); + } + + { + /* Tank Turret */ + std::vector turret_vertices; + std::vector turret_indices; + const int precision = 60; + const float inv_precision = 1.0f / float(precision); + for (int i = 0; i < precision; i++) { + auto theta = (float(i) + 0.5f) * inv_precision; + theta *= glm::pi() * 2.0f; + auto sin_theta = std::sin(theta); + auto cos_theta = std::cos(theta); + turret_vertices.push_back({{sin_theta * 0.5f, cos_theta * 0.5f}, + {0.0f, 0.0f}, + {0.7f, 0.7f, 0.7f, 1.0f}}); + turret_indices.push_back(i); + turret_indices.push_back((i + 1) % precision); + turret_indices.push_back(precision); + } + turret_vertices.push_back( + {{0.0f, 0.0f}, {0.0f, 0.0f}, {0.7f, 0.7f, 0.7f, 1.0f}}); + turret_vertices.push_back( + {{-0.1f, 0.0f}, {0.0f, 0.0f}, {0.7f, 0.7f, 0.7f, 1.0f}}); + turret_vertices.push_back( + {{0.1f, 0.0f}, {0.0f, 0.0f}, {0.7f, 0.7f, 0.7f, 1.0f}}); + turret_vertices.push_back( + {{-0.1f, 1.2f}, {0.0f, 0.0f}, {0.7f, 0.7f, 0.7f, 1.0f}}); + turret_vertices.push_back( + {{0.1f, 1.2f}, {0.0f, 0.0f}, {0.7f, 0.7f, 0.7f, 1.0f}}); + turret_indices.push_back(precision + 1 + 0); + turret_indices.push_back(precision + 1 + 1); + turret_indices.push_back(precision + 1 + 2); + turret_indices.push_back(precision + 1 + 1); + turret_indices.push_back(precision + 1 + 2); + turret_indices.push_back(precision + 1 + 3); + tank_turret_model_index = + mgr->RegisterModel(turret_vertices, turret_indices); + } + } +} + +void Tank::Render() { + battle_game::SetTransformation(position_, rotation_); + battle_game::SetTexture(0); + battle_game::SetColor(game_core_->GetPlayerColor(player_id_)); + battle_game::DrawModel(tank_body_model_index); + battle_game::SetRotation(turret_rotation_); + battle_game::DrawModel(tank_turret_model_index); +} + +void Tank::Update() { + TankMove(3.0f, glm::radians(180.0f)); + TurretRotate(); + Fire(); +} + +void Tank::TankMove(float move_speed, float rotate_angular_speed) { + auto player = game_core_->GetPlayer(player_id_); + if (player) { + auto &input_data = player->GetInputData(); + glm::vec2 offset{0.0f}; + if (input_data.key_down[GLFW_KEY_W]) { + offset.y += 1.0f; + } + if (input_data.key_down[GLFW_KEY_S]) { + offset.y -= 1.0f; + } + float speed = move_speed * GetSpeedScale(); + offset *= kSecondPerTick * speed; + auto new_position = + position_ + glm::vec2{glm::rotate(glm::mat4{1.0f}, rotation_, + glm::vec3{0.0f, 0.0f, 1.0f}) * + glm::vec4{offset, 0.0f, 0.0f}}; + if (!game_core_->IsBlockedByObstacles(new_position)) { + game_core_->PushEventMoveUnit(id_, new_position); + } + float rotation_offset = 0.0f; + if (input_data.key_down[GLFW_KEY_A]) { + rotation_offset += 1.0f; + } + if (input_data.key_down[GLFW_KEY_D]) { + rotation_offset -= 1.0f; + } + rotation_offset *= kSecondPerTick * rotate_angular_speed * GetSpeedScale(); + game_core_->PushEventRotateUnit(id_, rotation_ + rotation_offset); + } +} + +void Tank::TurretRotate() { + auto player = game_core_->GetPlayer(player_id_); + if (player) { + auto &input_data = player->GetInputData(); + auto diff = input_data.mouse_cursor_position - position_; + if (glm::length(diff) < 1e-4) { + turret_rotation_ = rotation_; + } else { + turret_rotation_ = std::atan2(diff.y, diff.x) - glm::radians(90.0f); + } + } +} + +void Tank::Fire() { + if (fire_count_down_ == 0) { + auto player = game_core_->GetPlayer(player_id_); + if (player) { + auto &input_data = player->GetInputData(); + if (input_data.mouse_button_down[GLFW_MOUSE_BUTTON_LEFT]) { + auto velocity = Rotate(glm::vec2{0.0f, 20.0f}, turret_rotation_); + GenerateBullet( + position_ + Rotate({0.0f, 1.2f}, turret_rotation_), + turret_rotation_, GetDamageScale(), velocity); + fire_count_down_ = kTickPerSecond; // Fire interval 1 second. + } + } + } + if (fire_count_down_) { + fire_count_down_--; + } +} + +bool Tank::IsHit(glm::vec2 position) const { + position = WorldToLocal(position); + return position.x > -0.8f && position.x < 0.8f && position.y > -1.0f && + position.y < 1.0f && position.x + position.y < 1.6f && + position.y - position.x < 1.6f; +} + +const char *Tank::UnitName() const { + return "Tiny Tank"; +} + +const char *Tank::Author() const { + return "LazyJazz"; +} +} // namespace battle_game::unit diff --git a/src/battle_game/core/units/tiny_tank.h b/src/battle_game/core/units/tiny_tank.h new file mode 100644 index 00000000..7727dce9 --- /dev/null +++ b/src/battle_game/core/units/tiny_tank.h @@ -0,0 +1,23 @@ +#pragma once +#include "battle_game/core/unit.h" + +namespace battle_game::unit { +class Tank : public Unit { + public: + Tank(GameCore *game_core, uint32_t id, uint32_t player_id); + void Render() override; + void Update() override; + [[nodiscard]] bool IsHit(glm::vec2 position) const override; + + protected: + void TankMove(float move_speed, float rotate_angular_speed); + void TurretRotate(); + void Fire(); + [[nodiscard]] const char *UnitName() const override; + [[nodiscard]] const char *Author() const override; + + float turret_rotation_{0.0f}; + uint32_t fire_count_down_{0}; + uint32_t mine_count_down_{0}; +}; +} // namespace battle_game::unit diff --git a/src/battle_game/core/units/units.h b/src/battle_game/core/units/units.h new file mode 100644 index 00000000..c1193717 --- /dev/null +++ b/src/battle_game/core/units/units.h @@ -0,0 +1,3 @@ +#pragma once + +#include "battle_game/core/units/tiny_tank.h" diff --git a/src/battle_game/graphics/CMakeLists.txt b/src/battle_game/graphics/CMakeLists.txt new file mode 100644 index 00000000..72f34f60 --- /dev/null +++ b/src/battle_game/graphics/CMakeLists.txt @@ -0,0 +1,12 @@ +set(lib_name battle_game_${COMPONENT_NAME}_lib) +list(APPEND LIBRARY_LIST ${lib_name}) + +add_library(${lib_name}) +file(GLOB source_files *.cpp *.h) +target_sources(${lib_name} PUBLIC ${source_files}) +target_link_libraries(${lib_name} PUBLIC grassland absl::strings) +target_include_directories(${lib_name} PUBLIC ${BATTLE_GAME_EXTERNAL_INCLUDE_DIRS} ${BATTLE_GAME_INCLUDE_DIR}) + +set(LIBRARY_LIST ${LIBRARY_LIST} PARENT_SCOPE) + +target_compile_definitions(${lib_name} PUBLIC BATTLE_GAME_ASSETS_DIR="${BATTLE_GAME_ASSETS_DIR}/") diff --git a/src/battle_game/graphics/README.md b/src/battle_game/graphics/README.md new file mode 100644 index 00000000..30321535 --- /dev/null +++ b/src/battle_game/graphics/README.md @@ -0,0 +1,134 @@ +# Graphics + +这部分代码你一般情况下不需要进行修改, +它们主要用于辅助你进行视觉效果的绘制 + +## 资产管理器 + +绘制图形需要涉及模型和纹理等资源, +这些绘图资源应避免重复加载以致占用不必要的额外内存空间。 +因此我们引入资产管理器辅助管理绘图资源,并方便地进行重复利用。 +你如果对绘图相关的知识不熟悉,可以不看这一章节的内容,利用下一章节中更直接的操作手段利用纹理进行绘制。 + +```c++ +static AssetsManager *AssetsManager::GetInstance(); +``` + +- 这个静态函数用于获得资产管理器对象的指针 + +```c++ +uint32_t RegisterModel(const std::vector &vertices, + const std::vector &indices); +``` + +- 这个函数用于注册一个新的模型 +- vertices 表示模型的顶点信息 +- indices 表示模型的索引信息 +- 返回值为注册的模型编号 +- 同样的内容请只注册一次,以避免浪费资源 + +```c++ +uint32_t RegisterTexture(const Texture &particle_texture); +``` + +- 这个函数用于注册一个新的纹理 +- particle_texture 表示注册的纹理图案 +- 返回值为注册的纹理编号,这个编号可以用于绘制操作前纹理的绑定 +- 同样的内容请只注册一次,以避免浪费资源 + +## 全局函数定义 + +```c++ +void SetColor(const glm::vec4 &color = glm::vec4{1.0f}); +``` + +- 设置全局绘制颜色 + +```c++ +glm::vec4 GetColor(); +``` + +- 获取当前设置的全局绘制颜色 + +```c++ +void SetPosition(glm::vec2 position); +``` + +- 设置全局绘制位置 + +```c++ +void SetRotation(float rotation); +``` + +- 设置全局绘制朝向 + +```c++ +glm::vec2 GetPosition(); +``` + +- 获取全局绘制位置 + +```c++ +float GetRotation(); +``` + +- 获取全局绘制朝向 + +```c++ +void SetScale(glm::vec2 scale); +``` + +- 设置全局模型拉伸倍率信息 + +```c++ +glm::vec2 GetScale(); +``` + +- 获取全局模型拉伸倍率信息 + +```c++ +void SetTransformation(glm::vec2 position, + float rotation = 0.0f, + glm::vec2 scale = glm::vec2{1.0f}); +``` + +- 统一设置变换信息 +- 相当于一起调用 SetPosition, SetRotation, SetScale 函数 + +```c++ +uint32_t RegisterTexture(const std::string &file_path); +``` + +- 根据图片文件注册一个新的纹理 +- `file_path` 表示图片文件的路径 +- 同样的内容请只注册一次,以避免浪费资源 + +```c++ +uint32_t SetTexture(const std::string &file_path); +``` + +- 根据图片文件的路径设置绑定的纹理 + +```c++ +void SetTexture(uint32_t texture_id = 0); +``` + +- 根据纹理注册编号设置绑定的纹理 + +```c++ +uint32_t GetTexture(); +``` + +- 获取当前绑定的纹理编号 + +```c++ +void DrawModel(uint32_t model_id = 0); +``` + +- 根据当前绑定的全局设置,绘制对应编号的注册模型 + +```c++ +void DrawTexture(const std::string &file_path); +``` + +- 根据当前绑定的全局设置,绘制 `file_path` 路径对应的图片到本地空间为 $[-1, 1]^2$ 的矩形区域内 diff --git a/src/battle_game/graphics/assets_manager.cpp b/src/battle_game/graphics/assets_manager.cpp new file mode 100644 index 00000000..cfc171a6 --- /dev/null +++ b/src/battle_game/graphics/assets_manager.cpp @@ -0,0 +1,75 @@ +#include "battle_game/graphics/assets_manager.h" + +namespace battle_game { +AssetsManager *AssetsManager::GetInstance() { + static AssetsManager assets_manager; + return &assets_manager; +} + +uint32_t AssetsManager::RegisterModel(const std::vector &vertices, + const std::vector &indices) { + assets_synced_ = false; + models_.emplace_back(vertices, indices); + return models_.size() - 1; +} + +AssetsManager::AssetsManager() { + textures_.emplace_back(1, 1, glm::vec4{1.0f}); + textures_.emplace_back(kParticleTextureSize, kParticleTextureSize); + current_texture_id_ = 1; + texture_infos_.push_back({0, 0, 1, 1, 0}); + models_.emplace_back( + std::vector{ + {{-1.0f, 1.0f}, {0.0f, 0.0f}, {1.0f, 1.0f, 1.0f, 1.0f}}, + {{-1.0f, -1.0f}, {0.0f, 1.0f}, {1.0f, 1.0f, 1.0f, 1.0f}}, + {{1.0f, 1.0f}, {1.0f, 0.0f}, {1.0f, 1.0f, 1.0f, 1.0f}}, + {{1.0f, -1.0f}, {1.0f, 1.0f}, {1.0f, 1.0f, 1.0f, 1.0f}}}, + std::vector{0, 1, 2, 1, 2, 3}); +} + +TextureInfo AssetsManager::GetTextureSpace(uint32_t width, uint32_t height) { + TextureInfo texture_info{}; + texture_info.width = width; + texture_info.height = height; + width++; + height++; + while (current_head_x_ + width >= kParticleTextureSize || + current_head_y_ + height >= kParticleTextureSize) { + if (current_head_x_ + width >= kParticleTextureSize) { + current_head_x_ = 0; + current_head_y_ = current_bottom_y_; + } else if (current_head_y_ + height >= kParticleTextureSize) { + current_head_x_ = 0; + current_head_y_ = 0; + current_bottom_y_ = 0; + current_texture_id_ = textures_.size(); + textures_.emplace_back(kParticleTextureSize, kParticleTextureSize); + } + } + current_bottom_y_ = std::max(current_bottom_y_, current_head_y_ + height); + texture_info.texture_id = current_texture_id_; + texture_info.x = current_head_x_; + texture_info.y = current_head_y_; + current_head_x_ += width; + return texture_info; +} + +uint32_t AssetsManager::RegisterTexture(const Texture &particle_texture) { + uint32_t result = texture_infos_.size(); + auto texture_info = GetTextureSpace(particle_texture.GetWidth(), + particle_texture.GetHeight()); + texture_infos_.push_back(texture_info); + auto &texture = textures_[texture_info.texture_id]; + auto x = std::lround(texture_info.x); + auto y = std::lround(texture_info.y); + auto buffer = particle_texture.GetBuffer(); + auto tex_buffer = texture.GetBuffer(); + for (auto dy = 0; dy < particle_texture.GetHeight(); dy++) { + std::memcpy(tex_buffer + (dy + y) * texture.GetWidth() + x, + buffer + dy * particle_texture.GetWidth(), + particle_texture.GetWidth() * sizeof(glm::vec4)); + } + assets_synced_ = false; + return result; +} +} // namespace battle_game diff --git a/src/battle_game/graphics/assets_manager.h b/src/battle_game/graphics/assets_manager.h new file mode 100644 index 00000000..7a5cc202 --- /dev/null +++ b/src/battle_game/graphics/assets_manager.h @@ -0,0 +1,54 @@ +#pragma once +#include "battle_game/graphics/model.h" +#include "battle_game/graphics/texture.h" +#include "battle_game/graphics/util.h" + +namespace battle_game { +constexpr uint32_t kParticleTextureSize = 4096; +class AssetsManager { + public: + AssetsManager(); + static AssetsManager *GetInstance(); + uint32_t RegisterModel(const std::vector &vertices, + const std::vector &indices); + uint32_t RegisterTexture(const Texture &particle_texture); + + std::vector &GetModels() { + return models_; + } + [[nodiscard]] const std::vector &GetModels() const { + return models_; + } + std::vector &GetTextures() { + return textures_; + } + [[nodiscard]] const std::vector &GetParticleTextures() const { + return textures_; + } + std::vector &GetTextureInfos() { + return texture_infos_; + } + [[nodiscard]] const std::vector &GetTextureInfos() const { + return texture_infos_; + } + + bool &GetSyncState() { + return assets_synced_; + } + [[nodiscard]] bool GetSyncState() const { + return assets_synced_; + } + + private: + TextureInfo GetTextureSpace(uint32_t width, uint32_t height); + std::vector textures_; + std::vector texture_infos_; + uint32_t current_texture_id_; + uint32_t current_head_x_{0}; + uint32_t current_head_y_{0}; + uint32_t current_bottom_y_{0}; + + std::vector models_; + bool assets_synced_{false}; +}; +} // namespace battle_game diff --git a/src/battle_game/graphics/graphics.cpp b/src/battle_game/graphics/graphics.cpp new file mode 100644 index 00000000..dfcbefdb --- /dev/null +++ b/src/battle_game/graphics/graphics.cpp @@ -0,0 +1,120 @@ +#include "battle_game/graphics/graphics.h" + +namespace battle_game { +namespace { +std::vector object_settings_; +std::vector texture_infos_; +std::vector model_ids_; +TextureInfo current_texture_info_; +uint32_t current_texture_id_; +ObjectSettings current_object_settings_; +glm::vec2 current_position_; +glm::vec2 current_scale_; +float current_rotation_; +AssetsManager *mgr{}; + +void UpdateObjectSettingsTransformMatrix() { + current_object_settings_.local_to_world = + glm::translate(glm::mat4{1.0f}, glm::vec3{current_position_, 0.0f}) * + glm::rotate(glm::mat4{1.0f}, current_rotation_, + glm::vec3{0.0f, 0.0f, 1.0f}) * + glm::scale(glm::mat4{1.0f}, glm::vec3{current_scale_, 1.0f}); +} +} // namespace + +void NewFrame() { + object_settings_.clear(); + texture_infos_.clear(); + model_ids_.clear(); + mgr = AssetsManager::GetInstance(); + current_texture_id_ = 0; + current_texture_info_ = mgr->GetTextureInfos()[0]; + current_object_settings_ = {glm::mat4{1.0f}, glm::vec4{1.0f}}; + current_position_ = {0.0f, 0.0f}; + current_rotation_ = 0.0f; + current_scale_ = glm::vec2{1.0f}; +} + +const std::vector &GetObjectSettings() { + return object_settings_; +} + +const std::vector &GetTextureInfos() { + return texture_infos_; +} + +const std::vector &GetModelIds() { + return model_ids_; +} + +void SetColor(const glm::vec4 &color) { + current_object_settings_.color = color; +} + +glm::vec4 GetColor() { + return current_object_settings_.color; +} + +void SetPosition(glm::vec2 position) { + current_position_ = position; + UpdateObjectSettingsTransformMatrix(); +} + +void SetRotation(float rotation) { + current_rotation_ = rotation; + UpdateObjectSettingsTransformMatrix(); +} + +void SetTexture(uint32_t texture_id) { + current_texture_id_ = texture_id; + current_texture_info_ = mgr->GetTextureInfos()[texture_id]; +} + +uint32_t GetTexture() { + return current_texture_id_; +} + +uint32_t RegisterTexture(const std::string &file_path) { + static std::map map_file_path_index; + if (!map_file_path_index.count(file_path)) { + map_file_path_index[file_path] = + mgr->RegisterTexture(Texture::Load(file_path)); + } + return map_file_path_index.at(file_path); +} + +uint32_t SetTexture(const std::string &file_path) { + auto id = RegisterTexture(file_path); + SetTexture(id); + return id; +} + +void DrawModel(uint32_t model_id) { + texture_infos_.push_back(current_texture_info_); + object_settings_.push_back(current_object_settings_); + model_ids_.push_back(model_id); +} + +void DrawTexture(const std::string &file_path) { + auto bak_texture_id = GetTexture(); + SetTexture(file_path); + DrawModel(0); + SetTexture(bak_texture_id); +} + +void SetScale(glm::vec2 scale) { + current_scale_ = scale; + UpdateObjectSettingsTransformMatrix(); +} + +glm::vec2 GetScale() { + return current_scale_; +} + +void SetTransformation(glm::vec2 position, float rotation, glm::vec2 scale) { + current_position_ = position; + current_rotation_ = rotation; + current_scale_ = scale; + UpdateObjectSettingsTransformMatrix(); +} +} // namespace battle_game diff --git a/src/battle_game/graphics/graphics.h b/src/battle_game/graphics/graphics.h new file mode 100644 index 00000000..64974139 --- /dev/null +++ b/src/battle_game/graphics/graphics.h @@ -0,0 +1,36 @@ +#pragma once +#include "battle_game/graphics/assets_manager.h" +#include "battle_game/graphics/texture.h" +#include "battle_game/graphics/util.h" + +namespace battle_game { + +void NewFrame(); + +void SetColor(const glm::vec4 &color = glm::vec4{1.0f}); +glm::vec4 GetColor(); + +void SetPosition(glm::vec2 position); +void SetRotation(float rotation); +glm::vec2 GetPosition(); +float GetRotation(); + +void SetScale(glm::vec2 scale); +glm::vec2 GetScale(); + +void SetTransformation(glm::vec2 position, + float rotation = 0.0f, + glm::vec2 scale = glm::vec2{1.0f}); + +uint32_t RegisterTexture(const std::string &file_path); +uint32_t SetTexture(const std::string &file_path); +void SetTexture(uint32_t texture_id = 0); +uint32_t GetTexture(); + +void DrawModel(uint32_t model_id = 0); +void DrawTexture(const std::string &file_path); + +const std::vector &GetObjectSettings(); +const std::vector &GetTextureInfos(); +const std::vector &GetModelIds(); +} // namespace battle_game diff --git a/src/battle_game/graphics/model.cpp b/src/battle_game/graphics/model.cpp new file mode 100644 index 00000000..364f5b55 --- /dev/null +++ b/src/battle_game/graphics/model.cpp @@ -0,0 +1,8 @@ +#include "battle_game/graphics/model.h" + +namespace battle_game { +Model::Model(const std::vector &vertices, + const std::vector &indices) + : vertices_(vertices), indices_(indices) { +} +} // namespace battle_game diff --git a/src/battle_game/graphics/model.h b/src/battle_game/graphics/model.h new file mode 100644 index 00000000..62fc162a --- /dev/null +++ b/src/battle_game/graphics/model.h @@ -0,0 +1,28 @@ +#pragma once +#include "battle_game/graphics/util.h" +#include "vector" + +namespace battle_game { +class Model { + public: + Model() = default; + Model(const std::vector &vertices, + const std::vector &indices); + std::vector &GetVertices() { + return vertices_; + } + [[nodiscard]] const std::vector &GetVertices() const { + return vertices_; + } + std::vector &GetIndices() { + return indices_; + } + [[nodiscard]] const std::vector &GetIndices() const { + return indices_; + } + + private: + std::vector vertices_; + std::vector indices_; +}; +} // namespace battle_game diff --git a/src/battle_game/graphics/texture.cpp b/src/battle_game/graphics/texture.cpp new file mode 100644 index 00000000..6627d5ba --- /dev/null +++ b/src/battle_game/graphics/texture.cpp @@ -0,0 +1,164 @@ +#include "battle_game/graphics//texture.h" + +#include "absl/strings/match.h" +#include "grassland/util/util.h" +#include "stb_image.h" +#include "stb_image_write.h" + +namespace battle_game { + +Texture::Texture(uint32_t width, + uint32_t height, + const glm::vec4 &color, + SampleType sample_type) { + width_ = width; + height_ = height; + buffer_.resize(width * height); + sample_type_ = sample_type; + std::fill(buffer_.data(), buffer_.data() + width * height, color); +} + +Texture::Texture(uint32_t width, + uint32_t height, + const glm::vec4 *color_buffer, + SampleType sample_type) { + width_ = width; + height_ = height; + buffer_.resize(width * height); + sample_type_ = sample_type; + std::memcpy(buffer_.data(), color_buffer, + sizeof(glm::vec4) * width_ * height_); +} + +void Texture::Resize(uint32_t width, uint32_t height) { + std::vector new_buffer(width * height); + for (int i = 0; i < std::min(height, height_); i++) { + std::memcpy(new_buffer.data() + width * i, buffer_.data() + width_ * i, + sizeof(glm::vec4) * std::min(width, width_)); + } + width_ = width; + height_ = height; + buffer_ = new_buffer; +} + +bool Texture::Load(const std::string &file_path, Texture &texture) { + int x, y, c; + if (absl::EndsWithIgnoreCase(file_path, ".hdr")) { + auto result = stbi_loadf(file_path.c_str(), &x, &y, &c, 4); + if (result) { + texture = Texture(x, y, reinterpret_cast(result), + SAMPLE_TYPE_LINEAR); + stbi_image_free(result); + } else { + return false; + } + } else { + auto result = stbi_load(file_path.c_str(), &x, &y, &c, 4); + if (result) { + std::vector convert_buffer(x * y); + const float inv_255 = 1.0f / 255.0f; + for (int i = 0; i < x * y; i++) { + convert_buffer[i] = glm::vec4{result[i * 4], result[i * 4 + 1], + result[i * 4 + 2], result[i * 4 + 3]} * + inv_255; + } + texture = Texture(x, y, convert_buffer.data(), SAMPLE_TYPE_LINEAR); + stbi_image_free(result); + } else { + return false; + } + } + return true; +} + +void Texture::Store(const std::string &file_path) { + if (absl::EndsWithIgnoreCase(file_path, ".hdr")) { + stbi_write_hdr(file_path.c_str(), width_, height_, 4, + reinterpret_cast(buffer_.data())); + } else { + std::vector convert_buffer(width_ * height_ * 4); + auto float_to_uint8 = [](float x) { + return std::min(std::max(std::lround(x * 255.0f), 0l), 255l); + }; + for (int i = 0; i < width_ * height_; i++) { + convert_buffer[i * 4] = float_to_uint8(buffer_[i].x); + convert_buffer[i * 4 + 1] = float_to_uint8(buffer_[i].y); + convert_buffer[i * 4 + 2] = float_to_uint8(buffer_[i].z); + convert_buffer[i * 4 + 3] = float_to_uint8(buffer_[i].w); + } + if (absl::EndsWithIgnoreCase(file_path, ".png")) { + stbi_write_png(file_path.c_str(), width_, height_, 4, + convert_buffer.data(), width_ * 4); + } else if (absl::EndsWithIgnoreCase(file_path, ".bmp")) { + stbi_write_bmp(file_path.c_str(), width_, height_, 4, + convert_buffer.data()); + } else if (absl::EndsWithIgnoreCase(file_path, ".jpg") || + absl::EndsWithIgnoreCase(file_path, ".jpeg")) { + stbi_write_jpg(file_path.c_str(), width_, height_, 4, + convert_buffer.data(), 100); + } else { + LAND_ERROR("Unknown file format \"{}\"", file_path.c_str()); + } + } +} + +void Texture::SetSampleType(SampleType sample_type) { + sample_type_ = sample_type; +} + +SampleType Texture::GetSampleType() const { + return sample_type_; +} + +glm::vec4 &Texture::operator()(int x, int y) { + x = std::min(int(width_ - 1), std::max(x, 0)); + y = std::min(int(height_ - 1), std::max(y, 0)); + return buffer_[y * width_ + x]; +} + +const glm::vec4 &Texture::operator()(int x, int y) const { + x = std::min(int(width_ - 1), std::max(x, 0)); + y = std::min(int(height_ - 1), std::max(y, 0)); + return buffer_[y * width_ + x]; +} + +glm::vec4 Texture::Sample(glm::vec2 tex_coord) const { + tex_coord = tex_coord - glm::floor(tex_coord); + tex_coord *= glm::vec2{width_, height_}; + if (sample_type_ == SAMPLE_TYPE_LINEAR) { + int x = std::lround(tex_coord.x - 0.5f); + int y = std::lround(tex_coord.y - 0.5f); + float fx = tex_coord.x - float(x); + float fy = tex_coord.y - float(y); + return operator()(x, y) * (1.0f - fx) * (1.0f - fy) + + operator()(x + 1, y) * (fx) * (1.0f - fy) + + operator()(x, y + 1) * (1.0f - fx) * (fy) + + operator()(x + 1, y + 1) * (fx) * (fy); + } else { + return operator()(std::lround(tex_coord.x), std::lround(tex_coord.y)); + } +} + +uint32_t Texture::GetWidth() const { + return width_; +} + +uint32_t Texture::GetHeight() const { + return height_; +} + +glm::vec4 *Texture::GetBuffer() { + return buffer_.data(); +} + +const glm::vec4 *Texture::GetBuffer() const { + return buffer_.data(); +} + +Texture Texture::Load(const std::string &file_path) { + Texture result; + Load(file_path, result); + return result; +} + +} // namespace battle_game diff --git a/src/battle_game/graphics/texture.h b/src/battle_game/graphics/texture.h new file mode 100644 index 00000000..6ad92aff --- /dev/null +++ b/src/battle_game/graphics/texture.h @@ -0,0 +1,40 @@ +#pragma once +#include "glm/glm.hpp" +#include "string" +#include "vector" + +namespace battle_game { + +enum SampleType { SAMPLE_TYPE_LINEAR = 0, SAMPLE_TYPE_NEAREST = 1 }; + +class Texture { + public: + Texture(uint32_t width = 1, + uint32_t height = 1, + const glm::vec4 &color = glm::vec4{0.0f}, + SampleType sample_type = SAMPLE_TYPE_LINEAR); + Texture(uint32_t width, + uint32_t height, + const glm::vec4 *color_buffer, + SampleType sample_type); + void Resize(uint32_t width, uint32_t height); + static bool Load(const std::string &file_path, Texture &texture); + static Texture Load(const std::string &file_path); + void Store(const std::string &file_path); + void SetSampleType(SampleType sample_type); + [[nodiscard]] SampleType GetSampleType() const; + glm::vec4 &operator()(int x, int y); + const glm::vec4 &operator()(int x, int y) const; + [[nodiscard]] glm::vec4 Sample(glm::vec2 tex_coord) const; + [[nodiscard]] uint32_t GetWidth() const; + [[nodiscard]] uint32_t GetHeight() const; + glm::vec4 *GetBuffer(); + [[nodiscard]] const glm::vec4 *GetBuffer() const; + + private: + uint32_t width_{}; + uint32_t height_{}; + std::vector buffer_; + SampleType sample_type_{SAMPLE_TYPE_LINEAR}; +}; +} // namespace battle_game diff --git a/src/battle_game/graphics/util.cpp b/src/battle_game/graphics/util.cpp new file mode 100644 index 00000000..9e6b92bc --- /dev/null +++ b/src/battle_game/graphics/util.cpp @@ -0,0 +1,21 @@ +#include "battle_game/graphics//util.h" + +namespace battle_game { +namespace { +vulkan_legacy::framework::Core *g_core = nullptr; +} + +void SetGlobalCore(vulkan_legacy::framework::Core *core) { + g_core = core; +} + +vulkan_legacy::framework::Core *GetGlobalCore() { + return g_core; +} + +glm::vec2 Rotate(glm::vec2 v, float angle) { + return glm::vec2{ + glm::rotate(glm::mat4{1.0f}, angle, glm::vec3{0.0f, 0.0f, 1.0f}) * + glm::vec4{v, 0.0f, 1.0f}}; +} +} // namespace battle_game diff --git a/src/battle_game/graphics/util.h b/src/battle_game/graphics/util.h new file mode 100644 index 00000000..4282dbce --- /dev/null +++ b/src/battle_game/graphics/util.h @@ -0,0 +1,35 @@ +#pragma once +#include "glm/glm.hpp" +#include "glm/gtc/matrix_transform.hpp" +#include "grassland/grassland.h" + +namespace battle_game { +using namespace grassland; +struct GlobalSettings { + glm::mat4 world_to_camera{}; +}; + +struct ObjectSettings { + glm::mat4 local_to_world{}; + glm::vec4 color{}; +}; + +struct ObjectVertex { + glm::vec2 position{}; + glm::vec2 tex_coord{}; + glm::vec4 color{}; +}; + +struct TextureInfo { + float x; + float y; + float width; + float height; + int texture_id; +}; + +void SetGlobalCore(vulkan_legacy::framework::Core *core); +vulkan_legacy::framework::Core *GetGlobalCore(); + +glm::vec2 Rotate(glm::vec2 v, float angle); +} // namespace battle_game diff --git a/vcpkg.json b/vcpkg.json new file mode 100644 index 00000000..4feb5b82 --- /dev/null +++ b/vcpkg.json @@ -0,0 +1,22 @@ +{ + "name": "grassland", + "version-string": "0.1.0", + "description": "Grassland", + "dependencies": [ + "glfw3", + "freetype", + "glm", + "stb", + "spdlog", + "imgui", + "abseil", + "gtest", + "vulkan-memory-allocator", + "glslang", + { + "name": "d3dx12", + "platform": "windows" + }, + "eigen3" + ] +}