diff --git a/breakout/.vscode/settings.json b/breakout/.vscode/settings.json new file mode 100644 index 0000000000000000000000000000000000000000..cc695570c08595296c4adc63018dc248a7955059 --- /dev/null +++ b/breakout/.vscode/settings.json @@ -0,0 +1,48 @@ +{ + "files.associations": { + "array": "cpp", + "atomic": "cpp", + "bit": "cpp", + "cctype": "cpp", + "clocale": "cpp", + "cmath": "cpp", + "compare": "cpp", + "concepts": "cpp", + "cstdarg": "cpp", + "cstddef": "cpp", + "cstdint": "cpp", + "cstdio": "cpp", + "cstdlib": "cpp", + "cwchar": "cpp", + "cwctype": "cpp", + "deque": "cpp", + "string": "cpp", + "unordered_map": "cpp", + "vector": "cpp", + "exception": "cpp", + "algorithm": "cpp", + "functional": "cpp", + "iterator": "cpp", + "memory": "cpp", + "memory_resource": "cpp", + "numeric": "cpp", + "optional": "cpp", + "random": "cpp", + "string_view": "cpp", + "system_error": "cpp", + "tuple": "cpp", + "type_traits": "cpp", + "utility": "cpp", + "initializer_list": "cpp", + "iosfwd": "cpp", + "limits": "cpp", + "new": "cpp", + "numbers": "cpp", + "ostream": "cpp", + "ranges": "cpp", + "stdexcept": "cpp", + "streambuf": "cpp", + "typeinfo": "cpp", + "map": "cpp" + } +} \ No newline at end of file diff --git a/breakout/Kconfig b/breakout/Kconfig new file mode 100644 index 0000000000000000000000000000000000000000..b34c07af636201ea95a17d884665a12be989a5c6 --- /dev/null +++ b/breakout/Kconfig @@ -0,0 +1,17 @@ +# +# For a description of the syntax of this configuration file, +# see the file kconfig-language.txt in the NuttX tools repository. +# + +config LVX_USE_DEMO_BREAKOUT + bool "breakout" + default n + +if LVX_USE_DEMO_BREAKOUT + config LVX_BREAKOUT_DATA_ROOT + string "BREAKOUT Data Root" + default "/data" + config LVX_BREAKOUT_STACKSIZE + int "LVX_BREAKOUT stack size" + default 65536 +endif diff --git a/breakout/Make.defs b/breakout/Make.defs new file mode 100644 index 0000000000000000000000000000000000000000..e5f6c177df689ddb2c039a5e0b4db1a0082dbce2 --- /dev/null +++ b/breakout/Make.defs @@ -0,0 +1,18 @@ +# +# Copyright (C) 2023 Xiaomi Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +ifneq ($(CONFIG_LVX_USE_DEMO_BREAKOUT),) +CONFIGURED_APPS += $(APPDIR)/packages/demos/breakout +endif diff --git a/breakout/Makefile b/breakout/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..f9dbb5020a7bd0dbd8063540a00407edd901c174 --- /dev/null +++ b/breakout/Makefile @@ -0,0 +1,32 @@ +# +# Copyright (C) 2022 Xiaomi Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +include $(APPDIR)/Make.defs + +ifeq ($(CONFIG_LVX_USE_DEMO_BREAKOUT), y) +PROGNAME = breakout +PRIORITY = 100 +STACKSIZE = $(CONFIG_LVX_BREAKOUT_STACKSIZE) +MODULE = $(CONFIG_LVX_USE_DEMO_BREAKOUT) +CSRCS += src/Audio/audio_ctl.c +CXXFLAGS += -I$(APPDIR)/packages/demos/breakout/src +CXXEXT := .cpp +CXXSRCS += $(shell find -L ./ -name "*.cpp" | grep -v breakout_main.cpp) + +MAINSRC = breakout_main.cpp +endif + +include $(APPDIR)/Application.mk diff --git a/breakout/Readme.md b/breakout/Readme.md new file mode 100644 index 0000000000000000000000000000000000000000..e83eb2998ad51d90391fb79a46fdcedcf2657b25 --- /dev/null +++ b/breakout/Readme.md @@ -0,0 +1,148 @@ +# 🎮 Breakout Game 《打砖块游戏》 +**“KUN生是旷野,不是轨道!”** +拥有对篮球无限热爱的KUN,在家族中独树一帜,正挥洒着汗水,勤奋努力的击打篮球撞碎砖块,缔造属于自己的篮球哲学。 + +🧱 基于OpenVela和LVGL开发的触屏Breakout打砖块游戏,实现了基本的游戏逻辑,增加了图片素材并实现了打击音效。 + + +在对应素材文件夹下,已通过`readme`和`LICENSE`文件详细标明了作者出处等信息,满足了相应的license协议要求,允许个人在作品及商业中使用,但不得对素材进行单独售卖。 + +以下是第二版游戏界面: + + + +# ✨ Features 特点 + +- **游戏区域**:使用 LVGL 创建全屏的游戏区域,并支持动态生成砖块。 +- **挡板与球的物理机制**:挡板通过触摸输入进行控制,球的运动具有物理效果,碰撞后反弹。 +- **砖块系统**:砖块具有血量,可被破坏,支持不同类型砖块的行为。 +- **音效反馈**:根据砖块的血量播放不同的音效。 +- **触摸输入**:支持触摸屏输入来控制挡板移动。 +- **游戏状态管理**:管理游戏状态(例如:PLAYING、GAME_OVER、PAUSE)以控制游戏流程。 + + +# 🖼️ Image Handling(图片处理) +## 1. libpng decoder(libpng 解码器) +好消息,OpenVela系统已经内置了该PNG解码器,仅需将`LIB_PNG`和`LV_USE_LIBPNG`配置为`yes`,即可轻松使用`lv_image_set_src`等函数读取使用`PNG`图片。使用方法可以参考:`https://lvgl.100ask.net/master/details/libs/libpng.html` +此外,这是libpng的github仓库链接:`https://github.com/pnggroup/libpng`,自己手动引入还是有难度。 + +## 2.Image Caching(图片缓存) +LVGL 支持将图像缓存到内存中,以提升图像组件在运行时的加载速度,特别适用于反复绘制的图像场景,如游戏背景、按钮图标等。 +在从外部读入图片过多,会有明显卡顿,可以使用图片缓存的方法,将图片读入内存。具体可以参考:`https://lvgl.100ask.net/master/details/main-components/image.html#overview-image-caching` + +# 🔊 Audio Handling(音频处理) +引用了packages_demos仓库下的music_player的音频控制文件`audio_ctl.c`和`audio_ctl.h`,通过`audio_ctl_init_nxaudio`加载音频文件,`audio_ctl_start`播放音频,`audio_ctl_stop`暂停音频,`audio_ctl_uninit_nxaudio`释放资源。目前实现了碰撞砖块发出音效,但还存在一些bug。 + + + +# 🚀 Getting Started 快速开始 + +首先进入模拟器配置 +## 1.配置模拟器(menuconfig) +```bash +./build.sh vendor/openvela/boards/vela/configs/goldfish-armeabi-v7a-ap menuconfig +``` + +使用`/`键进入搜索模式 +- 配置`LVX_USE_DEMO_BREAKOUT `为 `yes` +- `LVX_BREAKOUT_DATA_ROOT`的路径设置为`/data ` (Kconfig文件默认预设为 `/data`) +- `LVX_BREAKOUT_STACKSIZE`设置为65536 +- `LIB_PNG`和`LV_USE_LIBPNG`配置为`yes` (读取png图片资源) +- `AUDIO`和`AUDIOUTILS_NXAUDIO_LIB`配置为`yes` (读取wav音频文件) + +## 2.构建和清除构建文件 +### 开始构建 +```bash +./build.sh vendor/openvela/boards/vela/configs/goldfish-armeabi-v7a-ap -j$(nproc) +``` +### 清理构建产物 +```bash +./build.sh vendor/openvela/boards/vela/configs/goldfish-armeabi-v7a-ap distclean -j$(nproc) +``` +## 3.ADB推送更新关卡等资源(在模拟器运行状况下) +```bash +adb push apps/packages/demos/breakout/res /data/ +``` + +## 4.启动模拟器 +```bash +./emulator.sh vela +``` + +## 5.启动游戏 +```bash +breakout & +``` +--- +# ▶️ 启动游戏 + +在 main文件函数中初始化 LVGL后,调用: + +ballgame_start(); // 启动游戏 + + +# 🎮 Controls + +- 移动挡板:按住并拖动屏幕,挡板随手指水平移动。 + +- 发射小球:游戏开始时,轻触屏幕即可发射小球。 + +- 重新开始:掉球后点击 "Restart" 按钮重开一局。 + +# 📄 Level File Format (.dat) + +放置关卡文件: +将 .dat 文件放到资源路径下,例如: + + `breakout/res/map/level_1.dat` + +关卡文件为文本格式,使用字符描述砖块: +| 字符 | 说明 | 生命值 | +|------|----------------|--------| +| 空格 | 空(无生成) | 无 | +| `#` | 墙(不可破坏) | -1 | +| `A` | 绿色砖块 | 1 | +| `B` | 橙色砖块 | 2 | +| `C` | 紫色砖块 | 3 | +| `D` | 黄色砖块 | 4 | +| `E` | 蓝色砖块 | 5 | +| 未知类型 | 绿色砖块 | 1 | + +此处读取文件时,会将换行符`\n`,制表符`\r`,文件结尾符号`\0`也识别出来,需要注意排除。 + +关卡示例: +``` +####### +#A B A# +# C # +####### +``` + + +# 素材使用声明 +- 图片素材来源于`https://icon.sucai999.com/`以及`https://craftpix.net` +- 音频素材来源于`https://freesound.org/` + +**本项目中的图片资源可以在个人作品中使用,并且可以用于商业项目,前提是遵守相应的许可协议。** + +- **Microsoft 图片**:可在个人及商业项目中使用,需遵守 **MIT** 许可协议。 +- **Google 图片**:可在个人及商业项目中使用,需遵守 **Apache 2.0** 许可协议,并附带版权声明、许可证文本及修改声明。 +- **CraftPix.net 图片**:可用于个人及商业项目,但需遵守 **CraftPix 自定义许可协议**,保留版权信息并不得单独转售源文件。 +- **freesound音频**:本作品使用的音频文件皆为 **CC0(Creative Commons 0)** 授权的资源,表示该资源已被作者明确放弃所有版权及相关权利,可自由复制、修改、分发,亦可用于个人或商业用途,无需署名、无需许可、无任何限制。 + + + + +# 🛠️ TODO 未来的工作 + +- UI和界面美化 √ + +- 得分与生命系统 + +- 多种道具(如掉落多球分裂,加长板等) + +- 更新多关卡支持与切换 + +- 增加交互音效 √ + +- 更智能的反弹算法(目前在考虑box2d) diff --git a/breakout/breakout_main.cpp b/breakout/breakout_main.cpp new file mode 100644 index 0000000000000000000000000000000000000000..6b64e80c7d90d12a4535786598a24671089e944e --- /dev/null +++ b/breakout/breakout_main.cpp @@ -0,0 +1,78 @@ +/**************************************************** + * breakout_main.cpp + * Main program entry for the breakout game on NuttX OS. + ***************************************************/ + +#include +#include +#include +#include "src/breakout.h" +#include + +/** + * A helper function to initialize and run the libuv event loop. + * The loop handles input events, timers, and screen refreshes. + */ +static void lv_nuttx_uv_loop(uv_loop_t* loop, lv_nuttx_result_t* result) +{ + // Create a struct to pass integration info between NuttX and LVGL + lv_nuttx_uv_t uv_info; + void* data; + + // Initialize the libuv event loop + uv_loop_init(loop); + + // Prepare config info to pass to the LVGL-UV integration layer + lv_memset(&uv_info, 0, sizeof(uv_info)); + uv_info.loop = loop; + uv_info.disp = result->disp; // Pass in the initialized display device + uv_info.indev = result->indev; // Pass in the initialized input device (touchscreen) +#ifdef CONFIG_UINPUT_TOUCH + uv_info.uindev = result->utouch_indev; // If configured, also pass virtual user input device +#endif + data = lv_nuttx_uv_init(&uv_info); + uv_run(loop, UV_RUN_DEFAULT); + lv_nuttx_uv_deinit(&data); +} + +/************************************************** + * Application main entry function. + ***************************************************/ +extern "C" int breakout_main(int argc, FAR char *argv[]) +{ + lv_nuttx_dsc_t info; + lv_nuttx_result_t result; + uv_loop_t ui_loop; + lv_memset(&ui_loop, 0, sizeof(uv_loop_t)); + + // Safety check: ensure LVGL is not initialized multiple times + if (lv_is_initialized()) { + LV_LOG_ERROR("LVGL already initialized! aborting."); + return -1; + } + + // Initialize LVGL core library + lv_init(); + + // Initialize and bind NuttX display and input drivers + lv_nuttx_dsc_init(&info); + lv_nuttx_init(&info, &result); + + // Check if the display device was successfully initialized + if (result.disp == NULL) { + LV_LOG_ERROR("lv_demos initialization failure!"); + return 1; + } + + // Call the game start function to create the game UI and all objects + ballgame_start(); + + // Start the main event loop driving the entire UI and application + lv_nuttx_uv_loop(&ui_loop, &result); + + // After the application exits the loop, deinitialize all resources + lv_nuttx_deinit(&result); + lv_deinit(); + + return 0; +} diff --git a/breakout/res/audio/A4.wav b/breakout/res/audio/A4.wav new file mode 100644 index 0000000000000000000000000000000000000000..7137b6c75a10175d9f49910b797be1bbc8bc1632 Binary files /dev/null and b/breakout/res/audio/A4.wav differ diff --git a/breakout/res/audio/B4.wav b/breakout/res/audio/B4.wav new file mode 100644 index 0000000000000000000000000000000000000000..8d2e1e71a6c958ea360daa2645e03ce9b58b52e1 Binary files /dev/null and b/breakout/res/audio/B4.wav differ diff --git a/breakout/res/audio/C4.wav b/breakout/res/audio/C4.wav new file mode 100644 index 0000000000000000000000000000000000000000..2ba65afad1bfd257879b8ed31f5e00e5115613b1 Binary files /dev/null and b/breakout/res/audio/C4.wav differ diff --git a/breakout/res/audio/C5.wav b/breakout/res/audio/C5.wav new file mode 100644 index 0000000000000000000000000000000000000000..1269b363a7fa38d3006d682b5f0b378fc0921dad Binary files /dev/null and b/breakout/res/audio/C5.wav differ diff --git a/breakout/res/audio/D4.wav b/breakout/res/audio/D4.wav new file mode 100644 index 0000000000000000000000000000000000000000..6b18f52995739d98306249d267f957b79d50a1fc Binary files /dev/null and b/breakout/res/audio/D4.wav differ diff --git a/breakout/res/audio/E4.wav b/breakout/res/audio/E4.wav new file mode 100644 index 0000000000000000000000000000000000000000..b9dbeb5d1ce1b1f4b52a8b54e11c5a796c555994 Binary files /dev/null and b/breakout/res/audio/E4.wav differ diff --git a/breakout/res/audio/F4.wav b/breakout/res/audio/F4.wav new file mode 100644 index 0000000000000000000000000000000000000000..fcbd66beb0d0b5e4e4b0c605281d1d781dd2ca1a Binary files /dev/null and b/breakout/res/audio/F4.wav differ diff --git a/breakout/res/audio/G4.wav b/breakout/res/audio/G4.wav new file mode 100644 index 0000000000000000000000000000000000000000..4ffb2b850acb04241d88e42d55dd77816003e253 Binary files /dev/null and b/breakout/res/audio/G4.wav differ diff --git a/breakout/res/audio/LICENSE b/breakout/res/audio/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..a1974cbb9cb88a97653494b3d3f899ee22873653 --- /dev/null +++ b/breakout/res/audio/LICENSE @@ -0,0 +1,36 @@ +CC0 1.0 Universal + +CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER. +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer exclusive Copyright and Related Rights (defined below) upon the creator and subsequent owner(s) (each and all, an "owner") of an original work of authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for the purpose of contributing to a commons of creative, cultural and scientific works ("Commons") that the public can reliably and without fear of later claims of infringement build upon, modify, incorporate in other works, reuse and redistribute as freely as possible in any form whatsoever and for any purposes, including without limitation commercial purposes. These owners may contribute to the Commons to promote the ideal of a free culture and the further production of creative, cultural and scientific works, or to gain reputation or greater distribution for their Work in part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any expectation of additional consideration or compensation, the person associating CC0 with a Work (the "Affirmer"), to the extent that he or she is an owner of Copyright and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and publicly distribute the Work under its terms, with knowledge of his or her Copyright and Related Rights in the Work and the meaning and intended legal effect of CC0 on those rights. +1. Copyright and Related Rights. + +A Work made available under CC0 may be protected by copyright and related or neighboring rights ("Copyright and Related Rights"). Copyright and Related Rights include, but are not limited to, the following: + + the right to reproduce, adapt, distribute, perform, display, communicate, and translate a Work; + moral rights retained by the original author(s) and/or performer(s); + publicity and privacy rights pertaining to a person's image or likeness depicted in a Work; + rights protecting against unfair competition in regards to a Work, subject to the limitations in paragraph 4(a), below; + rights protecting the extraction, dissemination, use and reuse of data in a Work; + database rights (such as those arising under Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, and under any national implementation thereof, including any amended or successor version of such directive); and + other similar, equivalent or corresponding rights throughout the world based on applicable law or treaty, and any national implementations thereof. + +2. Waiver. + +To the greatest extent permitted by, but not in contravention of, applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and unconditionally waives, abandons, and surrenders all of Affirmer's Copyright and Related Rights and associated claims and causes of action, whether now known or unknown (including existing as well as future claims and causes of action), in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each member of the public at large and to the detriment of Affirmer's heirs and successors, fully intending that such Waiver shall not be subject to revocation, rescission, cancellation, termination, or any other legal or equitable action to disrupt the quiet enjoyment of the Work by the public as contemplated by Affirmer's express Statement of Purpose. +3. Public License Fallback. + +Should any part of the Waiver for any reason be judged legally invalid or ineffective under applicable law, then the Waiver shall be preserved to the maximum extent permitted taking into account Affirmer's express Statement of Purpose. In addition, to the extent the Waiver is so judged Affirmer hereby grants to each affected person a royalty-free, non transferable, non sublicensable, non exclusive, irrevocable and unconditional license to exercise Affirmer's Copyright and Related Rights in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "License"). The License shall be deemed effective as of the date CC0 was applied by Affirmer to the Work. Should any part of the License for any reason be judged legally invalid or ineffective under applicable law, such partial invalidity or ineffectiveness shall not invalidate the remainder of the License, and in such case Affirmer hereby affirms that he or she will not (i) exercise any of his or her remaining Copyright and Related Rights in the Work or (ii) assert any associated claims and causes of action with respect to the Work, in either case contrary to Affirmer's express Statement of Purpose. +4. Limitations and Disclaimers. + + No trademark or patent rights held by Affirmer are waived, abandoned, surrendered, licensed or otherwise affected by this document. + Affirmer offers the Work as-is and makes no representations or warranties of any kind concerning the Work, express, implied, statutory or otherwise, including without limitation warranties of title, merchantability, fitness for a particular purpose, non infringement, or the absence of latent or other defects, accuracy, or the present or absence of errors, whether or not discoverable, all to the greatest extent permissible under applicable law. + Affirmer disclaims responsibility for clearing rights of other persons that may apply to the Work or any use thereof, including without limitation any person's Copyright and Related Rights in the Work. Further, Affirmer disclaims responsibility for obtaining any necessary consents, permissions or other rights required for any use of the Work. + Affirmer understands and acknowledges that Creative Commons is not a party to this document and has no duty or obligation with respect to this CC0 or use of the Work. + + diff --git a/breakout/res/audio/readme.md b/breakout/res/audio/readme.md new file mode 100644 index 0000000000000000000000000000000000000000..f53e8e758fe1f8bb096b3e037ddc1f92c7b089be --- /dev/null +++ b/breakout/res/audio/readme.md @@ -0,0 +1,24 @@ + +All audio files are from: https://freesound.org/people/Teddy_Frost/ + +All are licensed under the Creative Commons 0 (CC0) license. + +You can copy, modify, distribute, and perform the sound, even for commercial purposes, all without the need of asking permission from the author. + + + + E4.wav by Teddy_Frost — https://freesound.org/s/334542/ — License: Creative Commons 0 + + F4.wav by Teddy_Frost — https://freesound.org/s/334541/ — License: Creative Commons 0 + + G4.wav by Teddy_Frost — https://freesound.org/s/334540/ — License: Creative Commons 0 + + B4.wav by Teddy_Frost — https://freesound.org/s/334539/ — License: Creative Commons 0 + + C4.wav by Teddy_Frost — https://freesound.org/s/334538/ — License: Creative Commons 0 + + C5.wav by Teddy_Frost — https://freesound.org/s/334537/ — License: Creative Commons 0 + + D4.wav by Teddy_Frost — https://freesound.org/s/334536/ — License: Creative Commons 0 + + A4.wav by Teddy_Frost — https://freesound.org/s/334534/ — License: Creative Commons 0 diff --git a/breakout/res/fonts/MiSans-Normal.ttf b/breakout/res/fonts/MiSans-Normal.ttf new file mode 100644 index 0000000000000000000000000000000000000000..3199d464141d0599dd1269a3fef4264a829c6da0 Binary files /dev/null and b/breakout/res/fonts/MiSans-Normal.ttf differ diff --git a/breakout/res/fonts/MiSans-Semibold.ttf b/breakout/res/fonts/MiSans-Semibold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..83c1a532632f582550d48bae117d758191ad1800 Binary files /dev/null and b/breakout/res/fonts/MiSans-Semibold.ttf differ diff --git a/breakout/res/icons/KUN.png b/breakout/res/icons/KUN.png new file mode 100644 index 0000000000000000000000000000000000000000..2efcde2c9a73633e2fefab9d5ab647e03add6b4d Binary files /dev/null and b/breakout/res/icons/KUN.png differ diff --git a/breakout/res/icons/LICENSE b/breakout/res/icons/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..fa127e844fff8bb29492eaf221c04d33c33b660f --- /dev/null +++ b/breakout/res/icons/LICENSE @@ -0,0 +1,53 @@ +# LICENSE + +The copyright and license statements for the image resources used in this project are as follows: + +--- + +## Microsoft Corporation Image Resources (MIT License) + +Images: ball.png, brick_#.png, brick_A.png, brick_B.png, brick_C.png, brick_D.png, brick_E.png, brick_F.png + +Copyright Notice: +Copyright (c) Microsoft Corporation + +License: +MIT License + +Full license text: https://opensource.org/licenses/MIT + +--- + +## Google Inc Image Resources (Apache License 2.0) + +Image: KUN.png + +Copyright Notice: +Copyright 2023 Google Inc + +License: +Apache License, Version 2.0 + +Full license text: https://www.apache.org/licenses/LICENSE-2.0 + +--- + +## CraftPix.net Image Resources (CraftPix.net License) + +Image: background.png + +Copyright Notice: +© CraftPix.net - 2D & 3D Game Assets + +License details: +Please refer to https://craftpix.net/file-licenses/ + +--- + +## Usage Notes + +- The image resources included in this project are subject to the above different licenses; please comply with the corresponding license terms. +- Both MIT and Apache 2.0 licenses allow commercial use but require preserving copyright and license notices. +- Use of CraftPix assets must strictly follow their official license terms; direct resale of source files is prohibited. + + diff --git a/breakout/res/icons/background_1.png b/breakout/res/icons/background_1.png new file mode 100644 index 0000000000000000000000000000000000000000..aa7e66e7c84a48b36da34d9623c852357f4d0a28 Binary files /dev/null and b/breakout/res/icons/background_1.png differ diff --git a/breakout/res/icons/background_2.png b/breakout/res/icons/background_2.png new file mode 100644 index 0000000000000000000000000000000000000000..248c3846b0c644e82e6bc18b47055b3906114602 Binary files /dev/null and b/breakout/res/icons/background_2.png differ diff --git a/breakout/res/icons/background_3.png b/breakout/res/icons/background_3.png new file mode 100644 index 0000000000000000000000000000000000000000..f6bfe203f58483abcc07c249b061b624b93c2c02 Binary files /dev/null and b/breakout/res/icons/background_3.png differ diff --git a/breakout/res/icons/ball.png b/breakout/res/icons/ball.png new file mode 100644 index 0000000000000000000000000000000000000000..4c43f9ea92b4616d9ff331f50d02c4f436384484 Binary files /dev/null and b/breakout/res/icons/ball.png differ diff --git a/breakout/res/icons/brick_#.png b/breakout/res/icons/brick_#.png new file mode 100644 index 0000000000000000000000000000000000000000..280a99c575379949c483db66309a87c86518f0c7 Binary files /dev/null and b/breakout/res/icons/brick_#.png differ diff --git a/breakout/res/icons/brick_A.png b/breakout/res/icons/brick_A.png new file mode 100644 index 0000000000000000000000000000000000000000..db7a95b4af9029dfae6a515545ccd89a38296ceb Binary files /dev/null and b/breakout/res/icons/brick_A.png differ diff --git a/breakout/res/icons/brick_B.png b/breakout/res/icons/brick_B.png new file mode 100644 index 0000000000000000000000000000000000000000..4c6609435fba41c40ed738f060c315d6b9839a9d Binary files /dev/null and b/breakout/res/icons/brick_B.png differ diff --git a/breakout/res/icons/brick_C.png b/breakout/res/icons/brick_C.png new file mode 100644 index 0000000000000000000000000000000000000000..ed398d6f4f84a1731b5a27d843a2da00757b11be Binary files /dev/null and b/breakout/res/icons/brick_C.png differ diff --git a/breakout/res/icons/brick_D.png b/breakout/res/icons/brick_D.png new file mode 100644 index 0000000000000000000000000000000000000000..a77025c84dd17b8e6387d554e89c7518e17e24f8 Binary files /dev/null and b/breakout/res/icons/brick_D.png differ diff --git a/breakout/res/icons/brick_E.png b/breakout/res/icons/brick_E.png new file mode 100644 index 0000000000000000000000000000000000000000..247696da70a88849564a3e5601ed9e64737a1747 Binary files /dev/null and b/breakout/res/icons/brick_E.png differ diff --git a/breakout/res/icons/brick_F.png b/breakout/res/icons/brick_F.png new file mode 100644 index 0000000000000000000000000000000000000000..91f011f2f8ee20c3d0c8133e620761b0319ae326 Binary files /dev/null and b/breakout/res/icons/brick_F.png differ diff --git a/breakout/res/icons/readme.md b/breakout/res/icons/readme.md new file mode 100644 index 0000000000000000000000000000000000000000..5942cc36c39794aefb1d55709b92dbefa1dc3a5b --- /dev/null +++ b/breakout/res/icons/readme.md @@ -0,0 +1,29 @@ +1. Microsoft Images (MIT License) + + Images: ball.png, brick_#.png, brick_A.png, brick_B.png, brick_C.png, brick_D.png, brick_E.png, brick_F.png + + Author: Microsoft Corporation + + License: MIT License + +2. Google Images (Apache 2.0 License) + + Image: KUN.png + + Author: Google Inc + + License: https://www.apache.org/licenses/LICENSE-2.0 + +The Apache 2.0 License requires including copyright notices, the license text, and the NOTICE file (if any), stating any modifications, and allows commercial use. + +3. CraftPix.net Images (CraftPix Custom License) + + Image: background_1.png, bcakground_2.png, bcakground_3.png + + Author: CraftPix.net + + License: https://craftpix.net/file-licenses/ + +The CraftPix license generally requires retaining copyright information, prohibits separate resale of source files, and allows use in commercial projects; please refer to their official license details for specifics. + + diff --git a/breakout/res/map/level_1.dat b/breakout/res/map/level_1.dat new file mode 100644 index 0000000000000000000000000000000000000000..aa63efc2b89f33ddaac25d5c9f1d57568bd44d72 --- /dev/null +++ b/breakout/res/map/level_1.dat @@ -0,0 +1,30 @@ + + + + EEE AEE EEA A + A A EE EE EEE + E E EE EE E + E E EE EE E + E E AE EE EEEE AE EAE E E EEEE E EEEE + E E EE E A A EE E EA AE A A E A EE + E E E E E E E E E E E E E EEE + E E E E EEEEEE E E E E EEEEEE E EEE EE + E E E E E E E EEE E E EE EE + A A EE E E A E E E E A E EE EEE + EEE E EE EEEE EEE EEE A EEEE EEEEE EE EEA + E + EEE + + C C C D C + DDDDDDDDDDD D DDDDDDDD D D DDDDDDDDDD + D D D D D DDDDD D D + D D D D DDDDD D D C D DD D + D D C D D D D D DDDDDDD D D D + DDDDDDDDDDDD D D DDDDD D D D D + D D C D D D DD D D D D + D D D D DDDDD D D D D D D D + D D D D DD D D D DD D + D D D D D D D D D C D C + D D D D D D D D DDCDDDCD DDDDDDDDDDD + DD D DD DD C D C + diff --git a/breakout/res/map/level_2.dat b/breakout/res/map/level_2.dat new file mode 100644 index 0000000000000000000000000000000000000000..ea35758910bdc726383eb7a259fc9681329c7fab --- /dev/null +++ b/breakout/res/map/level_2.dat @@ -0,0 +1,25 @@ + + + AABBBBCCDDAABB DDAABBBBCCDDAA + AABBBBCCDDAABB DDAABBBBCCDDAA + AABBBBCCDDAABB DDAABBBBCCDDAA + + + AABBBBCCDDAABB DDAABBBBCCDDAA + AABBBBCCDDAABB DDAABBBBCCDDAA + AABBBBCCDDAABB DDAABBBBCCDDAA + + + AABBBBCCDDAABB DDAABBBBCCDDAA + AABBBBCCDDAABB DDAABBBBCCDDAA + AABBBBCCDDAABB DDAABBBBCCDDAA + + + AABBBBE ECCDDAA + AABBBBE # # ECCDDAA + AABBBBE # # ECCDDAA + AABBBBE # # ECCDDAA + # # + # # +############### ############## +############### ############## diff --git a/breakout/screenshot/screenshot_1.png b/breakout/screenshot/screenshot_1.png new file mode 100644 index 0000000000000000000000000000000000000000..84845540a78f8984e881e2eb83c0c977e7fab5d0 Binary files /dev/null and b/breakout/screenshot/screenshot_1.png differ diff --git a/breakout/screenshot/screenshot_2.png b/breakout/screenshot/screenshot_2.png new file mode 100644 index 0000000000000000000000000000000000000000..ccea37c533510142d1a8483d72bac88d31d65727 Binary files /dev/null and b/breakout/screenshot/screenshot_2.png differ diff --git a/breakout/src/Audio/audio_ctl.c b/breakout/src/Audio/audio_ctl.c new file mode 100644 index 0000000000000000000000000000000000000000..765c6f437451bc80ac089d6757a758aa82a3edd6 --- /dev/null +++ b/breakout/src/Audio/audio_ctl.c @@ -0,0 +1,287 @@ +/********************* + * INCLUDES + *********************/ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "audio_ctl.h" + +#include + +/********************** + * STATIC PROTOTYPES + **********************/ + +static void app_dequeue_cb(unsigned long arg, + FAR struct ap_buffer_s *apb); +static void app_complete_cb(unsigned long arg); +static void app_user_cb(unsigned long arg, + FAR struct audio_msg_s *msg, FAR bool *running); + +/********************** + * STATIC VARIABLES + **********************/ + +static struct nxaudio_callbacks_s cbs = +{ + app_dequeue_cb, + app_complete_cb, + app_user_cb +}; + +/********************** + * STATIC FUNCTIONS + **********************/ + +static void app_dequeue_cb(unsigned long arg, FAR struct ap_buffer_s *apb) +{ + FAR audioctl_s *ctl = (FAR audioctl_s *)(uintptr_t)arg; + + if (!apb) + { + return; + } + + if (ctl->seek) { + lseek(ctl->fd, ctl->seek_position, SEEK_SET); + ctl->file_position = ctl->seek_position; + ctl->seek = false; + } + + apb->nbytes = read(ctl->fd, apb->samp, apb->nmaxbytes); + apb->curbyte = 0; + apb->flags = 0; + + while (0 < apb->nbytes && apb->nbytes < apb->nmaxbytes) + { + int n = apb->nmaxbytes - apb->nbytes; + int ret = read(ctl->fd, &apb->samp[apb->nbytes], n); + + if (0 >= ret) + { + break; + } + apb->nbytes += ret; + } + + if (apb->nbytes < apb->nmaxbytes) + { + close(ctl->fd); + ctl->fd = -1; + + return ; + } + + ctl->file_position += apb->nbytes; + + nxaudio_enqbuffer(&ctl->nxaudio, apb); +} + +static void app_complete_cb(unsigned long arg) +{ + /* Do nothing.. */ + + printf("Audio loop is Done\n"); +} + +static void app_user_cb(unsigned long arg, + FAR struct audio_msg_s *msg, FAR bool *running) +{ + /* Do nothing.. */ +} + +static FAR void *audio_loop_thread(pthread_addr_t arg) +{ + FAR audioctl_s *ctl = (FAR audioctl_s *)arg; + + nxaudio_start(&ctl->nxaudio); + nxaudio_msgloop(&ctl->nxaudio, &cbs, + (unsigned long)(uintptr_t)ctl); + + return NULL; +} + +/********************** + * GLOBAL FUNCTIONS + **********************/ + +FAR audioctl_s *audio_ctl_init_nxaudio(FAR const char *arg) +{ + FAR audioctl_s *ctl; + int ret; + int i; + + ctl = (FAR audioctl_s *)malloc(sizeof(audioctl_s)); + if(ctl == NULL) + { + return NULL; + } + + ctl->seek = false; + ctl->seek_position = 0; + ctl->file_position = 0; + + ctl->fd = open(arg, O_RDONLY); + if (ctl->fd < 0) { + printf("can't open audio file\n"); + return NULL; + } + + read(ctl->fd, &ctl->wav, sizeof(ctl->wav)); + + ret = init_nxaudio(&ctl->nxaudio, ctl->wav.fmt.samplerate, + ctl->wav.fmt.bitspersample, + ctl->wav.fmt.numchannels); + if (ret < 0) + { + printf("init_nxaudio() return with error!!\n"); + return NULL; + } + + for (i = 0; i < ctl->nxaudio.abufnum; i++) + { + app_dequeue_cb((unsigned long)ctl, ctl->nxaudio.abufs[i]); + } + + ctl->state = AUDIO_CTL_STATE_INIT; + + return ctl; +} + +int audio_ctl_start(FAR audioctl_s *ctl) +{ + if (ctl == NULL) + return -EINVAL; + + if (ctl->state != AUDIO_CTL_STATE_INIT && ctl->state != AUDIO_CTL_STATE_PAUSE) + { + return -1; + } + + ctl->state = AUDIO_CTL_STATE_START; + + pthread_attr_t tattr; + struct sched_param sparam; + + pthread_attr_init(&tattr); + sparam.sched_priority = sched_get_priority_max(SCHED_FIFO) - 9; + pthread_attr_setschedparam(&tattr, &sparam); + pthread_attr_setstacksize(&tattr, 4096); + + pthread_create(&ctl->pid, &tattr, audio_loop_thread, + (pthread_addr_t)ctl); + + pthread_attr_destroy(&tattr); + pthread_setname_np(ctl->pid, "audioctl_thread"); + + return 0; +} + +int audio_ctl_pause(FAR audioctl_s *ctl) +{ + if (ctl == NULL) + return -EINVAL; + + if (ctl->state != AUDIO_CTL_STATE_START) + { + return -1; + } + + ctl->state = AUDIO_CTL_STATE_PAUSE; + + return nxaudio_pause(&ctl->nxaudio); +} + +int audio_ctl_resume(FAR audioctl_s *ctl) +{ + if (ctl == NULL) + return -EINVAL; + + if (ctl->state != AUDIO_CTL_STATE_PAUSE) + { + return -1; + } + + ctl->state = AUDIO_CTL_STATE_START; + + return nxaudio_resume(&ctl->nxaudio); +} + +int audio_ctl_seek(FAR audioctl_s *ctl, unsigned ms) +{ + if (ctl == NULL) + return -EINVAL; + + ctl->seek_position = ms * ctl->wav.fmt.samplerate * ctl->wav.fmt.bitspersample * ctl->wav.fmt.numchannels / 8; + ctl->seek = true; + + return 0; +} + +int audio_ctl_stop(FAR audioctl_s *ctl) +{ + if (ctl == NULL) + return -EINVAL; + + if (ctl->state != AUDIO_CTL_STATE_PAUSE && ctl->state != AUDIO_CTL_STATE_START) + { + return -1; + } + + ctl->state = AUDIO_CTL_STATE_STOP; + + nxaudio_stop(&ctl->nxaudio); + + if (ctl->pid > 0) + { + pthread_join(ctl->pid, NULL); + } + + return 0; +} + +int audio_ctl_set_volume(FAR audioctl_s *ctl, uint16_t vol) +{ + if (ctl == NULL) + return -EINVAL; + + return nxaudio_setvolume(&ctl->nxaudio, vol); +} + +int audio_ctl_get_position(FAR audioctl_s *ctl) +{ + if (ctl == NULL) + return -EINVAL; + + return ctl->file_position / (ctl->wav.fmt.bitspersample * ctl->wav.fmt.numchannels * ctl->wav.fmt.samplerate / 8); +} + +int audio_ctl_uninit_nxaudio(FAR audioctl_s *ctl) +{ + if (ctl == NULL) + return -EINVAL; + + if (ctl->state == AUDIO_CTL_STATE_NOP) + { + return 0; + } + + if (ctl->fd > 0) + { + close(ctl->fd); + ctl->fd = -1; + } + + fin_nxaudio(&ctl->nxaudio); + + free(ctl); + + return 0; +} diff --git a/breakout/src/Audio/audio_ctl.h b/breakout/src/Audio/audio_ctl.h new file mode 100644 index 0000000000000000000000000000000000000000..e7e60993b3da128a7917731c5f59195a718653f2 --- /dev/null +++ b/breakout/src/Audio/audio_ctl.h @@ -0,0 +1,85 @@ +#ifndef AUDIO_CTL_H +#define AUDIO_CTL_H + +#ifdef __cplusplus +extern "C" { +#endif + +/********************* + * INCLUDES + *********************/ +#include +#include + +enum { + AUDIO_CTL_STATE_NOP, + AUDIO_CTL_STATE_INIT, + AUDIO_CTL_STATE_START, + AUDIO_CTL_STATE_PAUSE, + AUDIO_CTL_STATE_STOP, +}; + +typedef struct wav_riff { + /* chunk "riff" */ + char chunkID[4]; /* "RIFF" */ + /* sub-chunk-size */ + uint32_t chunksize; /* 36 + subchunk2size */ + /* sub-chunk-data */ + char format[4]; /* "WAVE" */ +} riff_s; + +typedef struct wav_fmt { + /* sub-chunk "fmt" */ + char subchunk1ID[4]; /* "fmt " */ + /* sub-chunk-size */ + uint32_t subchunk1size; /* 16 for PCM */ + /* sub-chunk-data */ + uint16_t audioformat; /* PCM = 1*/ + uint16_t numchannels; /* Mono = 1, Stereo = 2, etc. */ + uint32_t samplerate; /* 8000, 44100, etc. */ + uint32_t byterate; /* = samplerate * numchannels * bitspersample/8 */ + uint16_t blockalign; /* = numchannels * bitspersample/8 */ + uint16_t bitspersample; /* 8bits, 16bits, etc. */ +} fmt_s; + +typedef struct wav_data { + /* sub-chunk "data" */ + char subchunk2ID[4]; /* "data" */ + /* sub-chunk-size */ + uint32_t subchunk2size; /* data size */ + /* sub-chunk-data */ + //Data_block_t block; +} data_s; + +typedef struct wav_fotmat { + riff_s riff; + fmt_s fmt; + data_s data; +} wav_s; + +typedef struct audioctl { + struct nxaudio_s nxaudio; + wav_s wav; + int fd; + int state; + pthread_t pid; + int seek; + uint32_t seek_position; + uint32_t file_position; +} audioctl_s; + +FAR audioctl_s *audio_ctl_init_nxaudio(FAR const char *arg); +int audio_ctl_start(FAR audioctl_s *ctl); +int audio_ctl_pause(FAR audioctl_s *ctl); +int audio_ctl_resume(FAR audioctl_s *ctl); +int audio_ctl_seek(FAR audioctl_s *ctl, unsigned ms); +int audio_ctl_stop(FAR audioctl_s *ctl); +int audio_ctl_set_volume(FAR audioctl_s *ctl, uint16_t vol); +int audio_ctl_get_position(FAR audioctl_s *ctl); +int audio_ctl_uninit_nxaudio(FAR audioctl_s *ctl); + +#ifdef __cplusplus +} /*extern "C"*/ +#endif + +#endif /* AUDIO_CTL_H */ diff --git a/breakout/src/Ball/Ball.cpp b/breakout/src/Ball/Ball.cpp new file mode 100644 index 0000000000000000000000000000000000000000..3f8d393b6b8e8d0068d019956e39927d346a9a05 --- /dev/null +++ b/breakout/src/Ball/Ball.cpp @@ -0,0 +1,115 @@ +#include "Ball.h" +#include "GameResourceManager/GameResourceManager.h" +#include + +// Define gravity +static const float GRAVITY = 350.0f; +// Define the maximum ball speed +static const float MAX_BALL_SPEED = 750.0f; + +Ball::Ball(lv_obj_t* parent, float radius) : m_state(State::HELD), m_radius(radius), m_gui_object(nullptr) { + GameResourceManager resourceManager; + auto ball_src = resourceManager.getIconSource("ball.png"); + + printf("DEBUG: ball image loaded and decoded to RGB565 for the first time.\n"); + + m_gui_object = lv_img_create(parent); + if (!m_gui_object) { + printf("Failed to create lv_img object\n"); + return; + } + if(ball_src) { + lv_img_set_src(m_gui_object, ball_src); + } + else{ + printf("Failed to load ball.png\n"); + } + lv_obj_set_size(m_gui_object, m_radius * 2, m_radius * 2); + lv_style_init(&m_style); + lv_style_set_bg_color(&m_style, lv_color_white()); + lv_style_set_radius(&m_style, LV_RADIUS_CIRCLE); + lv_style_set_bg_color(&m_style, lv_palette_main(LV_PALETTE_RED)); + lv_style_set_border_width(&m_style, 0); + lv_obj_add_style(m_gui_object, &m_style, 0); +} + +void Ball::update(float deltaTime) { + if (m_state != State::MOVING) { + return; + } + + // Apply gravity + m_vel.y += GRAVITY * deltaTime; + + // Calculate current speed squared + float speed_sq = m_vel.x * m_vel.x + m_vel.y * m_vel.y; + float max_speed_sq = MAX_BALL_SPEED * MAX_BALL_SPEED; + + // If current speed squared exceeds max speed squared + if (speed_sq > max_speed_sq) { + // Calculate actual current speed + float current_speed = sqrtf(speed_sq); + // Scale velocity vector proportionally to max speed + m_vel.x = (m_vel.x / current_speed) * MAX_BALL_SPEED; + m_vel.y = (m_vel.y / current_speed) * MAX_BALL_SPEED; + } + + // Update position based on the finalized velocity + m_pos.x += m_vel.x * deltaTime; + m_pos.y += m_vel.y * deltaTime; + + // Update the LVGL object's position on screen + lv_obj_set_pos(m_gui_object, (lv_coord_t)(m_pos.x - m_radius), (lv_coord_t)(m_pos.y - m_radius)); +} + +void Ball::stickToPaddle(const Rect& paddleRect) { + if (m_state != State::HELD) return; + m_pos.x = paddleRect.x + paddleRect.width / 2.0f; + m_pos.y = paddleRect.y - m_radius; + lv_obj_set_pos(m_gui_object, (lv_coord_t)(m_pos.x - m_radius), (lv_coord_t)(m_pos.y - m_radius)); +} + +void Ball::launch() { + if (m_state == State::HELD) { + m_state = State::MOVING; + m_vel = { 150.0f, -750.0f }; + } +} + +void Ball::bounceX() { + m_vel.x = -m_vel.x; +} + +void Ball::bounceY() { + m_vel.y = -m_vel.y; +} + +void Ball::boostSpeed(float factor) { + m_vel.x *= factor; + m_vel.y *= factor; +} + +void Ball::setPosition(const Vec2& pos) { + m_pos = pos; + lv_obj_set_pos(m_gui_object, (lv_coord_t)(m_pos.x - m_radius), (lv_coord_t)(m_pos.y - m_radius)); +} + +void Ball::setVelocity(const Vec2& vel) { + m_vel = vel; +} + +Ball::State Ball::getState() const { + return m_state; +} + +Vec2 Ball::getPosition() const { + return m_pos; +} + +Vec2 Ball::getVelocity() const { + return m_vel; +} + +float Ball::getRadius() const { + return m_radius; +} \ No newline at end of file diff --git a/breakout/src/Ball/Ball.h b/breakout/src/Ball/Ball.h new file mode 100644 index 0000000000000000000000000000000000000000..fdd6557722e30de69de4d5afb546cac9b67332d8 --- /dev/null +++ b/breakout/src/Ball/Ball.h @@ -0,0 +1,34 @@ +#ifndef BALL_H +#define BALL_H + +#pragma once + +#include "breakout_types.h" + + + +class Ball { +public: + enum class State { HELD, MOVING }; + Ball(lv_obj_t* parent, float radius); + void update(float deltaTime); + void stickToPaddle(const Rect& paddleRect); + void launch(); + void bounceX(); + void bounceY(); + void setPosition(const Vec2& pos); + void setVelocity(const Vec2& vel); + void boostSpeed(float factor); + Vec2 getVelocity() const; + State getState() const; + Vec2 getPosition() const; + float getRadius() const; +private: + State m_state; + Vec2 m_pos, m_vel; + float m_radius; + lv_obj_t* m_gui_object; + lv_style_t m_style; +}; + +#endif // BALL_H \ No newline at end of file diff --git a/breakout/src/Brick/Brick.cpp b/breakout/src/Brick/Brick.cpp new file mode 100644 index 0000000000000000000000000000000000000000..43a4311181ed12c4960786067e2783abc1f4acda --- /dev/null +++ b/breakout/src/Brick/Brick.cpp @@ -0,0 +1,128 @@ +/******************************************** + * Brick.cpp + * Implementation file for the Brick class + * Responsible for creating individual bricks, managing hit points, updating colors, and handling collisions. + *******************************************/ +#include "GameResourceManager/GameResourceManager.h" +#include "Brick.h" + +/***************************************** + * x Brick's X coordinate + * y Brick's Y coordinate + * width Brick's width (also used for height as bricks are square) + * type Character representing the brick type and its initial hit points + ****************************************/ +Brick::Brick(lv_obj_t* parent, float x, float y, float width, char type) + : m_is_active(true), + m_rect{x, y, width, width}, + m_gui_object(nullptr) +{ + // Set hit points based on brick type character + switch(type) { + case '#': m_hp = -1; break; // Indestructible wall + case 'A': m_hp = 1; break; + case 'B': m_hp = 2; break; + case 'C': m_hp = 3; break; + case 'D': m_hp = 4; break; + case 'E': m_hp = 5; break; + default: m_hp = 1; break; + } + + m_gui_object = lv_img_create(parent); + if (!m_gui_object) { + printf("Failed to create lv_img object\n"); + return; + } + updateImage(); + // Set position and size + lv_obj_set_pos(m_gui_object, x, y); + lv_obj_set_size(m_gui_object, width, width); + + // Add black border style + static lv_style_t border_style; + + lv_style_init(&border_style); + lv_style_set_bg_color(&border_style, lv_color_white()); + lv_style_set_border_width(&border_style, 1); + lv_style_set_border_color(&border_style, lv_color_black()); + lv_style_set_pad_all(&border_style, 0); + lv_obj_add_style(m_gui_object, &border_style, 0); +} + +/*************************************** + * Destructor automatically called when Brick object is deleted + * Ensures corresponding UI object is also removed from screen to prevent memory leaks. + ***************************************/ +Brick::~Brick() { + if (m_gui_object) { + lv_obj_del(m_gui_object); + m_gui_object = nullptr; + } +} + +/** + * Called when the ball hits the brick + */ +void Brick::onHit() { + // Only apply damage if hit points > 0 (i.e., not an indestructible wall) + if (m_hp > 0) { + m_hp--; // Decrease hit points by 1 + + if (m_hp == 0) { + // If hit points are depleted + m_is_active = false; // Mark brick as inactive, collision checks will ignore it + lv_obj_add_flag(m_gui_object, LV_OBJ_FLAG_HIDDEN); // Hide brick's UI object on screen + } else { + // If still has hit points, update color for visual feedback + updateImage(); + } + } +} + +/** + * Checks if the brick is currently active (not destroyed) + * Returns true if active, false if destroyed + */ +bool Brick::isActive() const { + return m_is_active; +} + +/** + * Gets the bounding box of the brick (collision box) + * Returns a Rect containing position and size + */ +Rect Brick::getBoundingBox() const { + return m_rect; +} + +int Brick::getHP() const { + return m_hp; +} + +/** + * Updates the brick's background image based on current hit points + */ +void Brick::updateImage() { + if (!m_gui_object) return; + + std::string image_filename; + switch (m_hp) { + case -1: image_filename = "brick_#.png"; break; // Wall: black + case 1: image_filename = "brick_A.png"; break; // HP=1: green + case 2: image_filename = "brick_B.png"; break; // HP=2: orange + case 3: image_filename = "brick_C.png"; break; // HP=3: purple + case 4: image_filename = "brick_D.png"; break; // HP=6: yellow + case 5: image_filename = "brick_E.png"; break; // HP=5: blue + default: image_filename = "brick_F.png"; break; // Damaged (blue-green) + } + + GameResourceManager resourceManager; + auto img_src = resourceManager.getIconSource(image_filename); + if(img_src) { + lv_img_set_src(m_gui_object, img_src); + } + else{ + printf("Failed to load brick.png\n"); + } + +} diff --git a/breakout/src/Brick/Brick.h b/breakout/src/Brick/Brick.h new file mode 100644 index 0000000000000000000000000000000000000000..bd9df2b1072d3b8f899032f5e4980dc8e17e2e3c --- /dev/null +++ b/breakout/src/Brick/Brick.h @@ -0,0 +1,23 @@ +#ifndef BRICK_H +#define BRICK_H + +#pragma once +#include "breakout_types.h" + +class Brick { +public: + Brick(lv_obj_t* parent, float x, float y, float width, char type); + ~Brick(); + void onHit(); + bool isActive() const; + Rect getBoundingBox() const; + int getHP() const; +private: + void updateImage(); + int m_hp; + bool m_is_active; + Rect m_rect; + lv_obj_t* m_gui_object; + lv_style_t m_style; +}; +#endif // BRICK_H \ No newline at end of file diff --git a/breakout/src/GameResourceManager/GameResourceManager.cpp b/breakout/src/GameResourceManager/GameResourceManager.cpp new file mode 100644 index 0000000000000000000000000000000000000000..5227174c7e777288c5521e6a3b4df9092ae0b04a --- /dev/null +++ b/breakout/src/GameResourceManager/GameResourceManager.cpp @@ -0,0 +1,221 @@ +/** + * GameResourceManager.cpp + * Implementation file for the game resource manager. + * Responsible for constructing resource paths, loading level maps, + * and managing the image resource cache. + * (Version: uses lv_draw_buf_dup and lv_draw_buf_destroy) + */ + +#include "GameResourceManager.h" +#include +#include "Brick/Brick.h" +#include "breakout.h" + +// Define the static member variable for image caching, now storing lv_draw_buf_t* +std::map GameResourceManager::s_imageCache; + +GameResourceManager::GameResourceManager() : m_audioCtl(nullptr) {} +GameResourceManager::~GameResourceManager() { + stopAudio(); +} + +/** + * @brief Frees all cached image resources. + * Equivalent to the `cleanup_resources` function from your example. + */ +void GameResourceManager::cleanupCache() { + printf("[ResourceManager] Cleaning up image cache, %zu items to delete.\n", s_imageCache.size()); + for (auto const& [key, buf] : s_imageCache) { + if (buf != nullptr) { + lv_draw_buf_destroy(buf); // Use LVGL function to destroy draw_buf + } + } + s_imageCache.clear(); + printf("[ResourceManager] Cache cleanup complete.\n"); +} + +/** + * @brief Retrieves an icon resource. Loads and caches it if not already cached. + * Core logic corresponding to your `app_create_main_page` example. + */ +const lv_draw_buf_t* GameResourceManager::getIconSource(const std::string& iconName) { + // 1. Check cache + auto it = s_imageCache.find(iconName); + if (it != s_imageCache.end()) { + return it->second; + } + + // 2. Load and decode + std::string fullPath = getIconsPath() + "/" + iconName; + + lv_image_decoder_dsc_t decoder_dsc; + lv_result_t res = lv_image_decoder_open(&decoder_dsc, fullPath.c_str(), NULL); + + if (res != LV_RESULT_OK) { + printf("[ERROR] Failed to open image decoder: %s\n", fullPath.c_str()); + return nullptr; + } + + // 3. Copy the decoded image data to a permanent draw_buf + lv_draw_buf_t* permanent_buf = lv_draw_buf_dup(decoder_dsc.decoded); + + // 4. Close decoder and free temporary resources + lv_image_decoder_close(&decoder_dsc); + + if (permanent_buf == nullptr) { + printf("[ERROR] Failed to duplicate draw buffer.\n"); + return nullptr; + } + + // 5. Cache and return + s_imageCache[iconName] = permanent_buf; + printf("[ResourceManager] New image cached: %s\n", iconName.c_str()); + + return permanent_buf; +} + +// --- Resource path accessors --- + +std::string GameResourceManager::getResPath() { + return std::string(RES_ROOT) + "/res"; +} + +std::string GameResourceManager::getMapPath() { + return getResPath() + "/map"; +} + +std::string GameResourceManager::getFontsPath() { + return getResPath() + "/fonts"; +} + +std::string GameResourceManager::getIconsPath() { + return getResPath() + "/icons"; +} + +std::string GameResourceManager::getAudioPath() { + return getResPath() + "/audio"; +} + +/** + * @brief Loads a map from file and populates the game brick list. + */ +bool GameResourceManager::loadMap(const std::string& mapName, Game* game) { + std::string fullPath = getMapPath() + "/" + mapName; + const int BRICK_COLS = 80; + const int BRICK_ROWS = 35; + const float BRICK_WIDTH = (float)GAME_AREA_WIDTH / BRICK_COLS; + FILE* file = fopen(fullPath.c_str(), "r"); + if (!file) { + return false; + } + char line[BRICK_COLS + 2]; + for (int row = 0; row < BRICK_ROWS; ++row) { + if (!fgets(line, sizeof(line), file)) { + break; + } + for (int col = 0; col < BRICK_COLS && line[col] != '\0'; ++col) { + char brickType = line[col]; + if (brickType != ' ' && brickType != '\n' && brickType != '\r') { + float x = col * BRICK_WIDTH; + float y = row * BRICK_WIDTH; + Brick* newBrick = new Brick(game->m_game_area, x, y, BRICK_WIDTH, brickType); + game->m_bricks.push_back(newBrick); + } + } + } + fclose(file); + return true; +} + +/** + * @brief Loads and displays a background image onto the parent object. + */ +void GameResourceManager::loadBackground(lv_obj_t* parent, const std::string& iconName) { + const lv_draw_buf_t* bg_src = getIconSource(iconName); + if (!bg_src) return; + + lv_obj_t* bg_img = lv_img_create(parent); + lv_img_set_src(bg_img, bg_src); // Use draw buffer directly + lv_obj_set_size(bg_img, GAME_AREA_WIDTH, GAME_AREA_HEIGHT); + lv_obj_align(bg_img, LV_ALIGN_TOP_LEFT, 0, 0); + lv_obj_move_background(bg_img); +} + +/** + * @brief Plays the corresponding audio based on the brick HP. + * + * This function selects the appropriate `.wav` file based on the brick's HP, + * initializes or reinitializes the audio controller, and starts audio playback. + * If the current audio is already playing for the given brick HP, it avoids reinitialization. + * + * @param brickHp The HP of the brick, used to select the correct audio file. + * @return true if the audio started successfully, false if an error occurred. + */ +bool GameResourceManager::playAudio(int brickHp) { + const char* wavFile = nullptr; + // Select appropriate audio file based on brick's HP + switch (brickHp) { + case -1: wavFile = "C4.wav"; break; // For brick HP -1, play C4.wav + case 1: wavFile = "E4.wav"; break; // For brick HP 1, play E4.wav + case 2: wavFile = "F4.wav"; break; // For brick HP 2, play F4.wav + case 3: wavFile = "G4.wav"; break; // For brick HP 3, play G4.wav + case 4: wavFile = "A4.wav"; break; // For brick HP 4, play A4.wav + case 5: wavFile = "B4.wav"; break; // For brick HP 5, play B4.wav + case 6: wavFile = "C5.wav"; break; // For brick HP 6, play C5.wav + default: wavFile = "C4.wav"; break; // Default case, play C4.wav + } + + // Build full audio file path + std::string fullPath = getAudioPath() + "/" + wavFile; + + if (!m_audioCtl) { + // If no audio control is initialized, initialize the audio controller + m_audioCtl = audio_ctl_init_nxaudio(fullPath.c_str()); + if (!m_audioCtl) { + //printf("[Audio] Failed to initialize audio: %s\n", fullPath.c_str()); + return false; + } + } else { + //printf("[Audio] Stopping and reinitializing audio controller...\n"); + audio_ctl_stop(m_audioCtl); // Stop the current audio + audio_ctl_uninit_nxaudio(m_audioCtl); // Uninitialize the current audio controller + + // Reinitialize the audio controller with the new file + m_audioCtl = audio_ctl_init_nxaudio(fullPath.c_str()); + if (!m_audioCtl) { + //printf("[Audio] Failed to reinitialize audio: %s\n", fullPath.c_str()); + return false; + } + } + + if (m_audioCtl) { + audio_ctl_start(m_audioCtl); // Start the audio playback + //printf("[Audio] Playing: %s\n", fullPath.c_str()); + return true; + } + + return false; +} +/** + * @brief Stops the currently playing audio and releases resources. + * + * This function stops the audio if it is currently playing, uninitializes the + * audio controller, and frees associated resources. + */ +void GameResourceManager::stopAudio() { + // Check if the audio controller is initialized + if (m_audioCtl != nullptr) { + //printf("[Audio] Stopping and uninitializing audio controller...\n"); + + // If audio is playing, stop it + audio_ctl_stop(m_audioCtl); + + // Uninitialize the audio controller and clean up + audio_ctl_uninit_nxaudio(m_audioCtl); + m_audioCtl = nullptr; + + //printf("[Audio] Audio stopped and cleaned up.\n"); + } else { + //printf("[Audio] No audio to stop.\n"); + } +} diff --git a/breakout/src/GameResourceManager/GameResourceManager.h b/breakout/src/GameResourceManager/GameResourceManager.h new file mode 100644 index 0000000000000000000000000000000000000000..07a6ff5b3194a6ea0c9c0f929c99e56779832184 --- /dev/null +++ b/breakout/src/GameResourceManager/GameResourceManager.h @@ -0,0 +1,39 @@ +#ifndef GAME_RESOURCE_MANAGER_H +#define GAME_RESOURCE_MANAGER_H + +#include +#include +#include "lvgl.h" +#include "Audio/audio_ctl.h" +#define RES_ROOT CONFIG_LVX_BREAKOUT_DATA_ROOT + +class Game; + +class GameResourceManager { +public: + GameResourceManager(); + ~GameResourceManager(); + + std::string getResPath(); + std::string getMapPath(); + std::string getFontsPath(); + std::string getIconsPath(); + std::string getAudioPath(); + + bool playAudio(int brickHp); + void stopAudio(); + + bool loadMap(const std::string& mapName, Game* game); + void loadBackground(lv_obj_t* parent, const std::string& iconName); + + const lv_draw_buf_t* getIconSource(const std::string& iconName); + + static void cleanupCache(); + +private: + + static std::map s_imageCache; + audioctl_s* m_audioCtl = nullptr; +}; + +#endif // GAME_RESOURCE_MANAGER_H \ No newline at end of file diff --git a/breakout/src/Paddle/Paddle.h b/breakout/src/Paddle/Paddle.h new file mode 100644 index 0000000000000000000000000000000000000000..9c192c2cafbd1392091d681d8f5b4bcdc294f880 --- /dev/null +++ b/breakout/src/Paddle/Paddle.h @@ -0,0 +1,34 @@ +#ifndef PADDLE_H +#define PADDLE_H + +#include "lvgl.h" + +#include "breakout_types.h" +class Paddle { +public: + // 构造函数 + Paddle(lv_obj_t* parent, float startX, float startY, float width, float height, Rect screenBounds); + + // 析构函数 + ~Paddle(); + + // 关键修复:添加之前缺失的所有成员函数的声明 + void update(float deltaTime); + Rect getBoundingBox() const; + void moveTo(float targetX); + void stopMovement(); + float getX() const; + float getY() const; + +private: + float m_x; + float m_y; + float m_width; + float m_height; + float m_targetX; + bool m_isMoving; + Rect m_screenBounds; + lv_obj_t* m_gui_object; // 指向LVGL图片对象的指针 +}; + +#endif // PADDLE_H diff --git a/breakout/src/Paddle/paddle.cpp b/breakout/src/Paddle/paddle.cpp new file mode 100644 index 0000000000000000000000000000000000000000..f5e83f8a1d6a232bd4cc2015e4c6a2a251125faf --- /dev/null +++ b/breakout/src/Paddle/paddle.cpp @@ -0,0 +1,98 @@ +/************************************** + * Paddle.cpp + * Implementation file for the player-controlled paddle class + * Responsible for paddle creation, updating, movement, and boundary checking logic. + *************************************/ +#include "Paddle.h" +#include "GameResourceManager/GameResourceManager.h" +#include +#include +#include + + + +Paddle::Paddle(lv_obj_t* parent, float startX, float startY, float width, float height, Rect screenBounds) + : m_x(startX), + m_y(startY), + m_width(width), + m_height(height), + m_targetX(0.0f), + m_isMoving(false), + m_screenBounds(screenBounds), + m_gui_object(nullptr) +{ + printf("DEBUG: Paddle constructor called.\n"); + + // Image data and descriptor must remain static to ensure their lifetime + + GameResourceManager resourceManager; + auto paddle_src = resourceManager.getIconSource("KUN.png"); + + printf("DEBUG: Paddle image requested from cache/file.\n"); + + m_gui_object = lv_img_create(parent); + if (!m_gui_object) { + printf("Failed to create lv_img object\n"); + return; + } + + if(paddle_src) { + lv_img_set_src(m_gui_object, paddle_src); + } + else{ + printf("Failed to load KUN.png\n"); + } + lv_obj_set_size(m_gui_object, (lv_coord_t)m_width, (lv_coord_t)m_height); + lv_obj_set_pos(m_gui_object, (lv_coord_t)m_x, (lv_coord_t)m_y); + + m_targetX = m_x + m_width / 2.0f; +} + +Paddle::~Paddle() { + printf("DEBUG: Paddle destructor called. Deleting lv_obj...\n"); + if (m_gui_object) { + lv_obj_del(m_gui_object); + m_gui_object = nullptr; + } +} + +void Paddle::update(float deltaTime) { + if (m_isMoving) { + float diff = m_targetX - (m_x + m_width / 2.0f); + float speed = 700.0f; // Movement speed, can be adjusted as needed + if (std::abs(diff) > 1.0f) { + float move = speed * deltaTime; + if (diff > 0) { + m_x += std::min(move, diff); + } else { + m_x += std::max(-move, diff); + } + + // Boundary check + m_x = std::max(m_screenBounds.x, std::min(m_x, m_screenBounds.x + m_screenBounds.width - m_width)); + + if (m_gui_object) { + lv_obj_set_x(m_gui_object, (lv_coord_t)m_x); + } + } else { + m_isMoving = false; + } + } +} + +Rect Paddle::getBoundingBox() const { + return {m_x, m_y, m_width, m_height}; +} + +void Paddle::moveTo(float targetX) { + m_targetX = targetX; + m_isMoving = true; +} + +void Paddle::stopMovement() { + m_isMoving = false; +} + +float Paddle::getX() const { return m_x; } + +float Paddle::getY() const { return m_y; } \ No newline at end of file diff --git a/breakout/src/breakout.cpp b/breakout/src/breakout.cpp new file mode 100644 index 0000000000000000000000000000000000000000..ed789a5bd2997596bc372d06755ec792899e3299 --- /dev/null +++ b/breakout/src/breakout.cpp @@ -0,0 +1,370 @@ +/****************************************************************** + * breakout.cpp + * Main game logic implementation file + * Contains all method implementations for the Game class, as well as + * the global game entry point ballgame_start(). + * The Game class manages the game state, object lifecycle, and core logic. + ********************************************************************/ + +#include "breakout.h" +#include "Paddle/Paddle.h" +#include "Ball/Ball.h" +#include "Brick/Brick.h" +#include "GameResourceManager/GameResourceManager.h" +#include +#include +#include +#include + +// Global pointer holding and managing the current unique game instance +Game* g_game_instance = nullptr; + +// ################################################# +// ## Implementation of Game class member functions +// ################################################# + +/** + * Constructor of the Game class + * @param parent The LVGL parent object on which the Game instance is created (usually the screen) + */ +Game::Game(lv_obj_t* parent) + : m_game_area(nullptr), + m_bricks(), + m_parent_screen(parent), + m_state(GameState::PLAYING), + m_game_timer(nullptr), + m_gameBounds(), + m_paddle(nullptr), + m_ball(nullptr), + m_resourceManager(nullptr) +{ + printf("[Game] Constructor called\n"); + init(); + printf("[Game] Init finished\n"); +} + +/** + * Destructor of the Game class + * Responsible for safely cleaning up all dynamically allocated resources upon object destruction to prevent memory leaks. + */ +Game::~Game() { + printf("[Game] Destructor called, starting resource cleanup.\n"); + + // 1. First, clean up the static resource cache + GameResourceManager::cleanupCache(); + if (m_resourceManager) { + m_resourceManager->stopAudio(); + delete m_resourceManager; + m_resourceManager = nullptr; + } + printf("[Game] Stopped audio\n"); + // 2. Delete the game timer + if (m_game_timer) { + lv_timer_del(m_game_timer); + m_game_timer = nullptr; + } + + // 3. Delete all brick objects + for (Brick* brick : m_bricks) { + delete brick; + } + m_bricks.clear(); + printf("[Game] Deleted bricks\n"); + // 4. Delete the paddle, ball, and resource manager instance + delete m_paddle; + delete m_ball; + + printf("[Game] Resource cleanup completed.\n"); +} +/** + * @brief Initialize all game elements, UI, and timers + */ +void Game::init() { + printf("[Game] Init Start\n"); + // Create the UI object serving as game background and container + m_game_area = lv_obj_create(m_parent_screen); + lv_obj_set_size(m_game_area, GAME_AREA_WIDTH, GAME_AREA_HEIGHT); + lv_obj_center(m_game_area); + lv_obj_set_style_bg_color(m_game_area, lv_color_white(), 0); + lv_obj_set_style_border_width(m_game_area, 0, 0); + lv_obj_set_style_radius(m_game_area, 0, 0); + lv_obj_set_style_pad_all(m_game_area, 0, 0); + lv_obj_clear_flag(m_game_area, LV_OBJ_FLAG_SCROLLABLE); + + // Define game logic boundaries (relative to m_game_area) + m_gameBounds = {0, 0, (float)GAME_AREA_WIDTH, (float)GAME_AREA_HEIGHT}; + + // Load level map + m_resourceManager = new GameResourceManager(); + m_resourceManager->loadBackground(m_game_area,"background_1.png"); + m_resourceManager->loadMap("level_1.dat", this); + + // Create game object instances + m_paddle = new Paddle(m_game_area, (GAME_AREA_WIDTH / 2.0f) - 50.0f, GAME_AREA_HEIGHT - 70.0f, 80.0f, 80.0f, m_gameBounds); + printf("[Game] Paddle created\n"); + m_ball = new Ball(m_game_area, 16.0f); + printf("[Game] Ball created\n"); + + // Create a transparent touch layer overlaying the game area to receive player input + lv_obj_t* touch_area = lv_obj_create(m_game_area); + lv_obj_set_size(touch_area, GAME_AREA_WIDTH, GAME_AREA_HEIGHT); + lv_obj_set_pos(touch_area, 0, 0); + lv_obj_set_style_bg_opa(touch_area, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(touch_area, 0, 0); + lv_obj_add_event_cb(touch_area, touch_area_event_cb, LV_EVENT_ALL, this); + lv_obj_move_background(touch_area); // Move touch layer to the back to avoid covering other objects + + // Create the main game loop timer + m_game_timer = lv_timer_create(game_timer_cb, GAME_TICK_PERIOD, this); + printf("[Game] Init End\n"); +} + +/** + * Game main loop timer callback + * @param timer Pointer to the LVGL timer + */ +void Game::game_timer_cb(lv_timer_t* timer) { + // Retrieve the Game instance pointer from user data + Game* game = static_cast(timer->user_data); + // State protection: do nothing if game is not in PLAYING state + if (game->m_state != GameState::PLAYING) return; + + const float deltaTime = (float)GAME_TICK_PERIOD / 1000.0f; + + // Update paddle position (reacting to player input) + game->m_paddle->update(deltaTime); + + // Handle ball states separately + if (game->m_ball->getState() == Ball::State::HELD) { + // HELD state: ball follows the paddle movement + game->m_ball->stickToPaddle(game->m_paddle->getBoundingBox()); + } else { + // MOVING state: update ball physics and perform collision detection + game->m_ball->update(deltaTime); + Vec2 ballPos = game->m_ball->getPosition(); + + // 1. Boundary collision detection + if (ballPos.x - game->m_ball->getRadius() < game->m_gameBounds.x || ballPos.x + game->m_ball->getRadius() > game->m_gameBounds.x + game->m_gameBounds.width) { + game->m_ball->bounceX(); + } + if (ballPos.y - game->m_ball->getRadius() < game->m_gameBounds.y) { + game->m_ball->bounceY(); + game->m_ball->boostSpeed(4.0f); + } + if (ballPos.y + game->m_ball->getRadius() > game->m_gameBounds.y + game->m_gameBounds.height) { + game->trigger_game_over(); + return; + } + + // 2. Collision detection with bricks + game->handleBallBrickCollision(); + + // 3. Collision detection with paddle + game->handleBallPaddleCollision(); + } +} +/** + * Handle collision between the ball and the bricks. + * Checks each active brick for collision with the ball. + * If a collision occurs, bounce the ball appropriately and apply damage or special logic. + */ +void Game::handleBallBrickCollision() { + Vec2 ballPos = m_ball->getPosition(); + + for (auto it = m_bricks.begin(); it != m_bricks.end(); ++it) { + Brick* brick = *it; + if (!brick->isActive()) continue; // Skip inactive bricks + if (!checkCollision(m_ball, brick)) continue; // Skip if no collision + + Rect brickRect = brick->getBoundingBox(); + Vec2 brickCenter = { + brickRect.x + brickRect.width / 2.0f, + brickRect.y + brickRect.height / 2.0f + }; + Vec2 offset = { + ballPos.x - brickCenter.x, + ballPos.y - brickCenter.y + }; + + // Calculate overlap distances on X and Y axes + float overlapX = m_ball->getRadius() + (brickRect.width / 2.0f) - fabsf(offset.x); + float overlapY = m_ball->getRadius() + (brickRect.height / 2.0f) - fabsf(offset.y); + + // Special case: Wall bricks (hp == -1) only bounce ball, do not get destroyed + if (brick->getHP() == -1) { + if (overlapX < overlapY) { + m_ball->bounceX(); // Bounce horizontally + // Correct ball position to avoid sticking inside the brick + ballPos.x = offset.x > 0 + ? brickRect.x + brickRect.width + m_ball->getRadius() + : brickRect.x - m_ball->getRadius(); + } else { + m_ball->bounceY(); // Bounce vertically + ballPos.y = offset.y > 0 + ? brickRect.y + brickRect.height + m_ball->getRadius() + : brickRect.y - m_ball->getRadius(); + } + m_ball->setPosition(ballPos); + m_ball->boostSpeed(4.0f); // Slightly increase ball speed after bounce + break; // Exit after first collision handled + } + m_resourceManager->playAudio(brick->getHP()); + // Normal brick logic: apply damage and bounce ball + brick->onHit(); // Reduce brick HP or mark as destroyed + + if (overlapX < overlapY) { + m_ball->bounceX(); + ballPos.x = offset.x > 0 + ? brickRect.x + brickRect.width + m_ball->getRadius() + : brickRect.x - m_ball->getRadius(); + } else { + m_ball->bounceY(); + ballPos.y = offset.y > 0 + ? brickRect.y + brickRect.height + m_ball->getRadius() + : brickRect.y - m_ball->getRadius(); + } + m_ball->setPosition(ballPos); + m_ball->boostSpeed(4.0f); + break; + } +} + + + + +/** + * Handle collision between the ball and the paddle. + * When collision is detected, the ball bounces off the paddle with an angle + * depending on the hit position on the paddle. + */ +void Game::handleBallPaddleCollision() { + if (!m_ball || !m_paddle) return; + + Vec2 ballPos = m_ball->getPosition(); + + if (checkCollision(m_ball, m_paddle)) { + Rect paddleRect = m_paddle->getBoundingBox(); + float paddleCenter = paddleRect.x + paddleRect.width / 2.0f; + + // Calculate hit factor relative to paddle center (-1 to 1) + float hitFactor = (ballPos.x - paddleCenter) / (paddleRect.width / 2.0f); + const float maxAngleFactor = 350.0f; // Max horizontal velocity factor + + Vec2 newVel = m_ball->getVelocity(); + newVel.x = hitFactor * maxAngleFactor; // Modify horizontal velocity based on hit position + + // Ensure the ball always bounces upward + if (newVel.y > 0) newVel.y = -newVel.y; + + m_ball->setVelocity(newVel); + + // Correct ball position to prevent it sticking inside the paddle + Vec2 correctedPos = ballPos; + correctedPos.y = paddleRect.y - m_ball->getRadius() - 1.0f; + m_ball->setPosition(correctedPos); + } +} +/** + * Touch event callback, handles player touch input (static member function) + */ +void Game::touch_area_event_cb(lv_event_t* e) { + Game* game = static_cast(lv_event_get_user_data(e)); + if (game->m_state != GameState::PLAYING) return; + + lv_event_code_t code = lv_event_get_code(e); + if (code == LV_EVENT_PRESSED || code == LV_EVENT_PRESSING) { + // On press or drag, move the paddle + lv_point_t screen_point; + lv_indev_get_point(lv_event_get_indev(e), &screen_point); + lv_coord_t game_area_x = lv_obj_get_x(game->m_game_area); + lv_coord_t local_x = screen_point.x - game_area_x; + game->m_paddle->moveTo(local_x); + } else if (code == LV_EVENT_RELEASED || code == LV_EVENT_PRESS_LOST) { + // On release, stop paddle movement and launch the ball + game->m_paddle->stopMovement(); + if (game->m_ball->getState() == Ball::State::HELD) { + game->m_ball->launch(); + } + } +} + +/** + * "Restart" button click event callback + */ +void Game::restart_button_event_cb(lv_event_t* e) { + + + // If an old game instance exists, delete it first to clean up resources + if (g_game_instance != nullptr) { + delete g_game_instance; + g_game_instance = nullptr; + } + + // Restarting the game just calls the global start function again + ballgame_start(); +} + +/** + * @brief Trigger the game over state and display UI + */ +void Game::trigger_game_over() { + m_state = GameState::GAME_OVER; + lv_obj_t* game_over_label = lv_label_create(m_parent_screen); + lv_label_set_text(game_over_label, "GAME OVER!"); + lv_obj_set_style_text_color(game_over_label, lv_color_black(), 0); + lv_obj_center(game_over_label); + + lv_obj_t* restart_btn = lv_button_create(m_parent_screen); + lv_obj_align_to(restart_btn, game_over_label, LV_ALIGN_OUT_BOTTOM_MID, -23, 20); + lv_obj_t* btn_label = lv_label_create(restart_btn); + lv_label_set_text(btn_label, "Restart"); + lv_obj_center(btn_label); + + lv_obj_add_event_cb(restart_btn, restart_button_event_cb, LV_EVENT_CLICKED, this); +} + +/** + * Collision detection helper - Ball and Paddle + */ +bool Game::checkCollision(const Ball* ball, const Paddle* paddle) { + Vec2 ballPos = ball->getPosition(); + float ballRadius = ball->getRadius(); + Rect paddleRect = paddle->getBoundingBox(); + float closestX = std::max(paddleRect.x, std::min(ballPos.x, paddleRect.x + paddleRect.width)); + float closestY = std::max(paddleRect.y, std::min(ballPos.y, paddleRect.y + paddleRect.height)); + float distanceX = ballPos.x - closestX; + float distanceY = ballPos.y - closestY; + return (distanceX * distanceX) + (distanceY * distanceY) < (ballRadius * ballRadius); +} + +/** + * Collision detection helper - Ball and Brick + */ +bool Game::checkCollision(const Ball* ball, const Brick* brick) { + Vec2 ballPos = ball->getPosition(); + float ballRadius = ball->getRadius(); + Rect brickRect = brick->getBoundingBox(); + float closestX = std::max(brickRect.x, std::min(ballPos.x, brickRect.x + brickRect.width)); + float closestY = std::max(brickRect.y, std::min(ballPos.y, brickRect.y + brickRect.height)); + float distanceX = ballPos.x - closestX; + float distanceY = ballPos.y - closestY; + return (distanceX * distanceX) + (distanceY * distanceY) < (ballRadius * ballRadius); +} + +// ################################################# +// ## Global startup function +// ################################################# + +/** + * Global game start/restart function + * Manages creation and destruction of the Game instance. + */ +void ballgame_start() { + lv_obj_t* screen = lv_screen_active(); + // Clean all old LVGL objects from the screen + lv_obj_clean(screen); + lv_obj_set_style_bg_color(screen, lv_color_black(), 0); + // Create a new game instance + g_game_instance = new Game(screen); +} + diff --git a/breakout/src/breakout.h b/breakout/src/breakout.h new file mode 100644 index 0000000000000000000000000000000000000000..91054d4d9a6fd2482fd9ca15017afb8f21f043c6 --- /dev/null +++ b/breakout/src/breakout.h @@ -0,0 +1,71 @@ +#ifndef BREAKOUT_H +#define BREAKOUT_H + +#pragma once + +#include "breakout_types.h" +#include // Include C++ vector container to manage list of bricks +#include // Include C++ string class to handle file paths + +class Paddle; +class Ball; +class Brick; +class GameResourceManager; + +// Define core game states: playing or game over +enum class GameState { PLAYING, GAME_OVER }; + +// --- Main Game class definition --- +/** + * Main Game class (Game) + * Encapsulates all game states, objects, and core logic. + * Creates and initializes a game session in the constructor, + * and automatically cleans up all resources in the destructor. + */ +class Game { +public: + // --- Constructor and Destructor --- + Game(lv_obj_t* parent); // Constructor to create a game instance + ~Game(); // Destructor to destroy game instance and release all resources + + // Exposed for external modules (e.g., GameResourceManager) to access and modify directly during map loading + lv_obj_t* m_game_area; // Pointer to the UI container for the game area + std::vector m_bricks; // Dynamic array holding all brick objects + +private: + // State and core UI pointers + lv_obj_t* m_parent_screen; // Pointer to the parent screen where the game resides + GameState m_state; // Current game state (PLAYING or GAME_OVER) + lv_timer_t* m_game_timer; // Game main loop timer + + // Game area and boundaries + Rect m_gameBounds; // Logical bounds of the game area + + // Game logic objects + Paddle* m_paddle; // Paddle object + Ball* m_ball; // Ball object + GameResourceManager* m_resourceManager; // Resource manager object + +private: + // --- Internal helper functions --- + void init(); // Initialize the game + void trigger_game_over(); // Handle game over state + bool checkCollision(const Ball* ball, const Brick* brick); // Collision detection (ball-brick) + bool checkCollision(const Ball* ball, const Paddle* paddle); // Collision detection (ball-paddle) + void handleBallBrickCollision(); + void handleBallPaddleCollision(); + + // --- Static callback functions --- + static void game_timer_cb(lv_timer_t* timer); // Main loop timer callback + static void touch_area_event_cb(lv_event_t* e); // Touch input event callback + static void restart_button_event_cb(lv_event_t* e); // Restart button click callback +}; + +// --- Global start function prototype --- +/** + * Global game start/restart function interface + * The only external entry point to start or restart the entire game. + */ +void ballgame_start(); + +#endif // BREAKOUT_H \ No newline at end of file diff --git a/breakout/src/breakout_types.h b/breakout/src/breakout_types.h new file mode 100644 index 0000000000000000000000000000000000000000..05edca789cb7c5018fca658cc0e3d1ef458f063c --- /dev/null +++ b/breakout/src/breakout_types.h @@ -0,0 +1,14 @@ +#pragma once + +#include "lvgl.h" + +// 2D vector struct, represents position and velocity +struct Vec2 { float x = 0.0f; float y = 0.0f; }; + +// Rectangle struct, represents position, size, and bounds +struct Rect { float x = 0.0f; float y = 0.0f; float width = 0.0f; float height = 0.0f; }; + +// Define global constants for game screen size and timer period +static const int GAME_AREA_WIDTH = 1200; +static const int GAME_AREA_HEIGHT = 760; +static const int GAME_TICK_PERIOD = 20; // Game logic update period (milliseconds) \ No newline at end of file