├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── app.ctn ├── app ├── config.ctn ├── core.ctn ├── input.ctn └── signals.ctn ├── bin ├── install.php ├── jit-play ├── play └── visu ├── bootstrap.php ├── composer.json ├── composer.lock ├── resources ├── background │ └── seamlessbg.png ├── shader │ ├── background.frag.glsl │ ├── background.vert.glsl │ ├── example_image.frag.glsl │ └── example_image.vert.glsl └── sprites │ ├── helpium.png │ ├── pipe.png │ ├── visuphpant.png │ ├── visuphpant2.png │ └── visuphpant3.png └── src ├── Component ├── GameCamera2DComponent.php ├── GlobalStateComponent.php ├── PlayerComponent.php └── SpriteComponent.php ├── Debug └── DebugTextOverlay.php ├── Game.php ├── Pass └── SpritePass.php ├── Renderer ├── BackgroundRenderer.php └── SpriteRenderer.php ├── Scene ├── BaseScene.php └── GameViewScene.php ├── SignalHandler └── WindowActionsHandler.php ├── Signals ├── ResetGameSignal.php └── SwitchToSceneSignal.php └── System ├── CameraSystem2D.php ├── FlappyPHPantSystem.php ├── PipeSystem.php └── RenderingSystem2D.php /.gitattributes: -------------------------------------------------------------------------------- 1 | *.obj filter=lfs diff=lfs merge=lfs -text 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /var/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Mario Döring 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FlappyPHPant 2 | 3 | A very simple Flappy Bird-like game written in PHP, built on [PHP-GLFW](http://github.com/mario-deluna/php-glfw) and the [VISU](https://github.com/phpgl/visu) framework. 4 | 5 |

6 | FlappyPHPant 2D PHP Game 7 |

8 | 9 | ## Installation 10 | 11 | **Make sure the php-glfw extension is installed and enabled.** 12 | 13 | If you havent done so already follow the instructions for your operating system here: 14 | * [MacOS](https://phpgl.net/getting-started/installation/install-macos.html) 15 | * [Linux](https://phpgl.net/getting-started/installation/install-linux.html) 16 | * [Windows](https://phpgl.net/getting-started/installation/install-windows.html) 17 | 18 | Then you can simply clone the project and install the dependencies with composer. 19 | 20 | ```bash 21 | git clone https://github.com/phpgl/flappyphpant.git 22 | cd flappyphpant 23 | composer install 24 | ``` 25 | 26 | ## Usage 27 | 28 | ```bash 29 | bash bin/play 30 | ``` 31 | 32 | * Press `space` to jump. 33 | * Press `R` to reset. 34 | * Press `F1` to disable the profiler and the debug overlay. 35 | _Note: the profiler takes a huge toll on performance as it forces the GPU to sync with the CPU after each render pass._ 36 | 37 | I really suck at this game, so if you surpass my score, please refrain from tweeting at me and making me feel ashamed. 38 | 39 | https://github.com/phpgl/flappyphpant/assets/956212/2f2a13e1-702f-416c-b060-9aa51f1fbe0c 40 | 41 | ## Features 42 | 43 | A lot of this is complete overkill for a simple Flappy Bird game, but I see this as an example project to demonstrate how you could build a 2D game with PHP-GLFW and VISU. 44 | 45 | Also, for time's sake, I cut a few corners, so the code is not as clean as I would like it to be. 46 | 47 | - **Decoupled Simulation from Rendering** 48 | 49 | The `render` and `update` functions are decoupled. 50 | This means the game simulation (aka `update()`) runs at a fixed rate, while the rendering (aka `render()`) runs as fast as possible. 51 | (Or, when vsync is enabled, at the refresh rate of the monitor.) 52 | The player movement is interpolated between the last and the current simulation step, allowing smooth movement even when the simulation is running significantly slower than the rendering. 53 | 54 | s 55 | 56 | FlappyPHPant specifically has a tick rate of `60ups` but can render at about `3000fps` with a resolution of `2560x1440` on an M1 MacBook Pro. 57 | 58 | _I was honestly really surprised at how good the frametimes are, considering the entire framework is written in PHP._ 59 | 60 | - **Proper 2D Camera** 61 | 62 | The camera, unlike the real Flappy Bird, is not fixed to a vertical resolution. 63 | The window can be resized to any resolution and aspect ratio you want, and the game should scale properly. 64 | 65 | * Support for High DPI displays means that on retina displays, the game will be rendered at a higher internal resolution. 66 | * The number of pipes rendered is based on the viewport and automatically adjusts to the window size. 67 | 68 | | Vertical | Horizontal | 69 | |------------|-------------| 70 | | | | 71 | 72 | - **Abstracted Input Handling** 73 | 74 | Input handling can get messy quickly; this example utilizes Input and Action maps to abstract the input handling and, theoretically, allow for easy remapping of the controls. 75 | 76 | ```php 77 | // apply jump 78 | if ($this->inputContext->actions->didButtonPress('jump')) { 79 | $playerComponent->velocity = $playerComponent->jumpForce; 80 | } 81 | ``` 82 | 83 | (I do understand how silly this is in a game where you basically just press one button.) 84 | 85 | - **Entity Component System** 86 | 87 | This example uses an Entity Component System to manage the game objects and share state between the different systems. 88 | 89 | ```php 90 | $playerEntity = $entities->create(); 91 | $entities->attach($playerEntity, new SpriteComponent('visuphpant2.png')); 92 | $entities->attach($playerEntity, new PlayerComponent()); 93 | $entities->attach($playerEntity, new Transform()); 94 | ``` 95 | 96 | This kind of game is unfortunately not the best example for an ECS. 97 | 98 | - **Simple Sprite Renderer** 99 | 100 | This project showcases a simple sprite renderer that can render individual sprites from a sprite sheet. 101 | This is used to render the animated player elephant as well as the pipes. It's nothing complex but should give you a starting point if you want to build a 2D game with VISU. 102 | 103 | - **Very Basic AABB Collision Detection** 104 | 105 | The collision detection is very basic and only checks for collisions between the player AABB and the pipe AABBs. 106 | It can be infuriating at times, as the elephant will collide with the pipes even if it looks like it should not. 107 | 108 | - **Text Rendering** 109 | 110 | Showcasing a simple example of how to render text labels. I know this might sound underwhelming, but text handling can be pretty darn annoying. 111 | -------------------------------------------------------------------------------- /app.ctn: -------------------------------------------------------------------------------- 1 | /** 2 | * This is the application's main container file. In other words, 3 | * it's the place where you want to specify your parameters and services. 4 | * 5 | * By default, all container files located in the `/app` directory are added 6 | * to the current namespace. This means that you can easily import these files 7 | * anywhere in your code by using the `import app/myfile` syntax. 8 | */ 9 | 10 | /** 11 | * Configuration 12 | * This includes the defaults, parameters, settings, and any other configuration-related aspects. 13 | */ 14 | import app/config 15 | 16 | /** 17 | * Core services 18 | * These are fundamental services like game loop, window, etc., that the application heavily relies on. 19 | */ 20 | import app/core 21 | 22 | /** 23 | * Input / action & control mapping 24 | * This involves the mapping of user inputs to certain actions or controls in your application. 25 | */ 26 | import app/input 27 | 28 | /** 29 | * Signal handlers 30 | * things in here are used to handle various events or signals that occur during the execution of your application. 31 | * They can be used to gracefully handle errors, interruptions, and other events that need specific actions when they occur. 32 | */ 33 | import app/signals 34 | -------------------------------------------------------------------------------- /app/config.ctn: -------------------------------------------------------------------------------- 1 | /** 2 | * Configuration 3 | * 4 | * ---------------------------------------------------------------------------- 5 | */ 6 | 7 | /** 8 | * Application basics 9 | */ 10 | :project.name: 'Flappyphphant' 11 | :project.version: 'v0.1' 12 | 13 | /** 14 | * Runtime 15 | */ 16 | :runtime.ticks_per_second: 60 17 | 18 | /** 19 | * Window setup 20 | */ 21 | // defaults 22 | :window.default_widht: 1280 23 | :window.default_height: 720 24 | 25 | // base window hints 26 | @window.main.hints: VISU\OS\WindowHints 27 | - setFocused(true) 28 | - setFocusOnShow(true) 29 | - setResizable(true) 30 | -------------------------------------------------------------------------------- /app/core.ctn: -------------------------------------------------------------------------------- 1 | /** 2 | * Window 3 | */ 4 | @window.main: VISU\OS\Window( 5 | :project.name, 6 | :window.default_widht, 7 | :window.default_height, 8 | @window.main.hints 9 | ) 10 | 11 | /** 12 | * VISU GL State 13 | */ 14 | @GL: VISU\Graphics\GLState 15 | 16 | /** 17 | * Main Game & Loop 18 | */ 19 | // the main game 20 | @game: App\Game(@container) 21 | 22 | // the game loop 23 | // we are tick the game 60 times a second 24 | @game_loop.main: VISU\Runtime\GameLoop(@game, :runtime.ticks_per_second) 25 | 26 | /** 27 | * Shader storage 28 | */ 29 | @shaders: VISU\Graphics\ShaderCollection(@GL, :visu.path.resources.shader) 30 | - enableVISUIncludes() 31 | - addVISUShaders() 32 | - scanShaderDirectory(:visu.path.resources.shader) 33 | 34 | /** 35 | * Profiler 36 | */ 37 | @profiler: VISU\Instrument\CompatGPUProfiler -------------------------------------------------------------------------------- /app/input.ctn: -------------------------------------------------------------------------------- 1 | /** 2 | * Base input handler 3 | */ 4 | @input: VISU\OS\Input(@window.main, @visu.dispatcher) 5 | 6 | /** 7 | * Input context mapper 8 | */ 9 | @input.context: VISU\OS\InputContextMap(@visu.dispatcher) 10 | - register('game', @input.actions.game) 11 | - switchTo('game') 12 | 13 | /** 14 | * Actions maps (aka key bindings) 15 | */ 16 | 17 | /** 18 | * Camera controls 19 | */ 20 | :input.action.map.game: { 21 | jump: '@Key::SPACE', 22 | reset: '@Key::R', 23 | } 24 | 25 | /** 26 | * Level Editor 27 | */ 28 | @input.actions.game: VISU\OS\InputActionMap() 29 | - importArrayMap(:input.action.map.game) -------------------------------------------------------------------------------- /app/signals.ctn: -------------------------------------------------------------------------------- 1 | /** 2 | * Window action handler 3 | */ 4 | @signal.handler.window_actions: App\SignalHandler\WindowActionsHandler 5 | = on: 'input.key', call: 'handleWindowKey' -------------------------------------------------------------------------------- /bin/install.php: -------------------------------------------------------------------------------- 1 | value : '') . $line . CLIColor::RESET->value . PHP_EOL; 28 | } 29 | } 30 | 31 | function printSeperator(string $char = '-', int $length = 80) { 32 | printLine(str_repeat($char, $length)); 33 | } 34 | 35 | /** 36 | * Install Setup 37 | * ---------------------------------------------------------------------------- 38 | */ 39 | 40 | printSeperator(); 41 | printLine('VISU Project Setup', 2, CLIColor::CYAN); 42 | printSeperator(); 43 | 44 | /** 45 | * Check for requirements 46 | * ---------------------------------------------------------------------------- 47 | */ 48 | // Check for PHP version > 8.1 49 | if (version_compare(PHP_VERSION, '8.1.0') < 0) { 50 | printLine('Your PHP version is ' . PHP_VERSION . ' but VISU requires at least PHP 8.1.0', 0, CLIColor::RED); 51 | exit(1); 52 | } 53 | 54 | // check if the "glfw" extension is installed 55 | if (!extension_loaded('glfw')) { 56 | printLine('The "glfw" extension is not installed.', 0, CLIColor::RED); 57 | printLine('Please goto https://phpgl.net and follow the installation instructions.', 0, CLIColor::RED); 58 | exit(1); 59 | } 60 | 61 | /** 62 | * Ask for setup values 63 | * ---------------------------------------------------------------------------- 64 | */ 65 | $projectName = ''; 66 | 67 | printLine('# Please enter the project name:', 0, CLIColor::YELLOW); 68 | 69 | while (empty($projectName)) { 70 | $projectName = readline('Project Name: '); 71 | } 72 | 73 | printLine('Great! Your project will be named "' . $projectName . '".', 0, CLIColor::GREEN); 74 | -------------------------------------------------------------------------------- /bin/jit-play: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | php -dopcache.enable_cli=1 -dopcache.jit_buffer_size=100M ./bin/play -------------------------------------------------------------------------------- /bin/play: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | get('game'); 28 | $game->start(); 29 | 30 | // clean up glfw 31 | glfwTerminate(); -------------------------------------------------------------------------------- /bin/visu: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | get('visu.command.cli_loader')->pass($argv); -------------------------------------------------------------------------------- /bootstrap.php: -------------------------------------------------------------------------------- 1 | =7.4" 25 | }, 26 | "require-dev": { 27 | "composer/composer": "^2.3", 28 | "phpstan/phpstan": "^1.5", 29 | "phpunit/phpunit": "^9.0", 30 | "vimeo/psalm": "5.x-dev" 31 | }, 32 | "type": "library", 33 | "autoload": { 34 | "psr-4": { 35 | "ClanCats\\Container\\": "src/" 36 | } 37 | }, 38 | "notification-url": "https://packagist.org/downloads/", 39 | "license": [ 40 | "MIT" 41 | ], 42 | "description": "ClanCats IoC Container.", 43 | "support": { 44 | "issues": "https://github.com/ClanCats/Container/issues", 45 | "source": "https://github.com/ClanCats/Container/tree/v1.3.5" 46 | }, 47 | "time": "2023-07-21T11:30:02+00:00" 48 | }, 49 | { 50 | "name": "league/climate", 51 | "version": "3.8.2", 52 | "source": { 53 | "type": "git", 54 | "url": "https://github.com/thephpleague/climate.git", 55 | "reference": "a785a3ac8f584eed4abd45e4e16fe64c46659a28" 56 | }, 57 | "dist": { 58 | "type": "zip", 59 | "url": "https://api.github.com/repos/thephpleague/climate/zipball/a785a3ac8f584eed4abd45e4e16fe64c46659a28", 60 | "reference": "a785a3ac8f584eed4abd45e4e16fe64c46659a28", 61 | "shasum": "" 62 | }, 63 | "require": { 64 | "php": "^7.3 || ^8.0", 65 | "psr/log": "^1.0 || ^2.0 || ^3.0", 66 | "seld/cli-prompt": "^1.0" 67 | }, 68 | "require-dev": { 69 | "mikey179/vfsstream": "^1.6.10", 70 | "mockery/mockery": "^1.4.2", 71 | "phpunit/phpunit": "^9.5.10" 72 | }, 73 | "suggest": { 74 | "ext-mbstring": "If ext-mbstring is not available you MUST install symfony/polyfill-mbstring" 75 | }, 76 | "type": "library", 77 | "autoload": { 78 | "psr-4": { 79 | "League\\CLImate\\": "src/" 80 | } 81 | }, 82 | "notification-url": "https://packagist.org/downloads/", 83 | "license": [ 84 | "MIT" 85 | ], 86 | "authors": [ 87 | { 88 | "name": "Joe Tannenbaum", 89 | "email": "hey@joe.codes", 90 | "homepage": "http://joe.codes/", 91 | "role": "Developer" 92 | }, 93 | { 94 | "name": "Craig Duncan", 95 | "email": "git@duncanc.co.uk", 96 | "homepage": "https://github.com/duncan3dc", 97 | "role": "Developer" 98 | } 99 | ], 100 | "description": "PHP's best friend for the terminal. CLImate allows you to easily output colored text, special formats, and more.", 101 | "keywords": [ 102 | "cli", 103 | "colors", 104 | "command", 105 | "php", 106 | "terminal" 107 | ], 108 | "support": { 109 | "issues": "https://github.com/thephpleague/climate/issues", 110 | "source": "https://github.com/thephpleague/climate/tree/3.8.2" 111 | }, 112 | "time": "2022-06-18T14:42:08+00:00" 113 | }, 114 | { 115 | "name": "phpgl/ide-stubs", 116 | "version": "dev-main", 117 | "source": { 118 | "type": "git", 119 | "url": "https://github.com/phpgl/ide-stubs.git", 120 | "reference": "39abde3eb8ca023a71f93b5e90bec2f4e9c6f5ac" 121 | }, 122 | "dist": { 123 | "type": "zip", 124 | "url": "https://api.github.com/repos/phpgl/ide-stubs/zipball/39abde3eb8ca023a71f93b5e90bec2f4e9c6f5ac", 125 | "reference": "39abde3eb8ca023a71f93b5e90bec2f4e9c6f5ac", 126 | "shasum": "" 127 | }, 128 | "require": { 129 | "php": ">=8.0" 130 | }, 131 | "default-branch": true, 132 | "type": "library", 133 | "notification-url": "https://packagist.org/downloads/", 134 | "license": [ 135 | "MIT", 136 | "Apache-2.0" 137 | ], 138 | "authors": [ 139 | { 140 | "name": "Mario Döring", 141 | "homepage": "https://github.com/mario-deluna" 142 | }, 143 | { 144 | "name": "Contributors", 145 | "homepage": "https://github.com/phpgl/ide-stubs/graphs/contributors" 146 | } 147 | ], 148 | "description": "IDE Stubs for PHP-GLFW / OpenGL. (PHPStorm, VSCode, etc.)", 149 | "homepage": "https://phpgl.net", 150 | "keywords": [ 151 | "3d", 152 | "Rendering", 153 | "autocomplete", 154 | "glfw", 155 | "opengl", 156 | "php-glfw", 157 | "phpstorm" 158 | ], 159 | "support": { 160 | "issues": "https://github.com/phpgl/ide-stubs/issues", 161 | "source": "https://github.com/phpgl/ide-stubs/tree/main" 162 | }, 163 | "time": "2023-03-14T23:03:57+00:00" 164 | }, 165 | { 166 | "name": "phpgl/visu", 167 | "version": "dev-master", 168 | "source": { 169 | "type": "git", 170 | "url": "https://github.com/phpgl/visu.git", 171 | "reference": "dc70a6d71cb54e1dbd7c51f6ab395d8bcb48eaaf" 172 | }, 173 | "dist": { 174 | "type": "zip", 175 | "url": "https://api.github.com/repos/phpgl/visu/zipball/dc70a6d71cb54e1dbd7c51f6ab395d8bcb48eaaf", 176 | "reference": "dc70a6d71cb54e1dbd7c51f6ab395d8bcb48eaaf", 177 | "shasum": "" 178 | }, 179 | "require": { 180 | "clancats/container": "^1.3", 181 | "league/climate": "^3.8", 182 | "php": ">=8.1" 183 | }, 184 | "require-dev": { 185 | "phpbench/phpbench": "^1.2", 186 | "phpgl/ide-stubs": "dev-main", 187 | "phpstan/phpstan": "^1.8", 188 | "phpunit/phpunit": "^9.5" 189 | }, 190 | "default-branch": true, 191 | "type": "library", 192 | "extra": { 193 | "container": { 194 | "@main": "visu.ctn" 195 | } 196 | }, 197 | "autoload": { 198 | "psr-4": { 199 | "VISU\\": "src/" 200 | } 201 | }, 202 | "notification-url": "https://packagist.org/downloads/", 203 | "license": [ 204 | "MIT" 205 | ], 206 | "description": "A Modern OpenGL Framework for PHP, ex php-game-framework.", 207 | "support": { 208 | "issues": "https://github.com/phpgl/visu/issues", 209 | "source": "https://github.com/phpgl/visu/tree/master" 210 | }, 211 | "time": "2023-08-06T22:33:52+00:00" 212 | }, 213 | { 214 | "name": "psr/log", 215 | "version": "3.0.0", 216 | "source": { 217 | "type": "git", 218 | "url": "https://github.com/php-fig/log.git", 219 | "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001" 220 | }, 221 | "dist": { 222 | "type": "zip", 223 | "url": "https://api.github.com/repos/php-fig/log/zipball/fe5ea303b0887d5caefd3d431c3e61ad47037001", 224 | "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001", 225 | "shasum": "" 226 | }, 227 | "require": { 228 | "php": ">=8.0.0" 229 | }, 230 | "type": "library", 231 | "extra": { 232 | "branch-alias": { 233 | "dev-master": "3.x-dev" 234 | } 235 | }, 236 | "autoload": { 237 | "psr-4": { 238 | "Psr\\Log\\": "src" 239 | } 240 | }, 241 | "notification-url": "https://packagist.org/downloads/", 242 | "license": [ 243 | "MIT" 244 | ], 245 | "authors": [ 246 | { 247 | "name": "PHP-FIG", 248 | "homepage": "https://www.php-fig.org/" 249 | } 250 | ], 251 | "description": "Common interface for logging libraries", 252 | "homepage": "https://github.com/php-fig/log", 253 | "keywords": [ 254 | "log", 255 | "psr", 256 | "psr-3" 257 | ], 258 | "support": { 259 | "source": "https://github.com/php-fig/log/tree/3.0.0" 260 | }, 261 | "time": "2021-07-14T16:46:02+00:00" 262 | }, 263 | { 264 | "name": "seld/cli-prompt", 265 | "version": "1.0.4", 266 | "source": { 267 | "type": "git", 268 | "url": "https://github.com/Seldaek/cli-prompt.git", 269 | "reference": "b8dfcf02094b8c03b40322c229493bb2884423c5" 270 | }, 271 | "dist": { 272 | "type": "zip", 273 | "url": "https://api.github.com/repos/Seldaek/cli-prompt/zipball/b8dfcf02094b8c03b40322c229493bb2884423c5", 274 | "reference": "b8dfcf02094b8c03b40322c229493bb2884423c5", 275 | "shasum": "" 276 | }, 277 | "require": { 278 | "php": ">=5.3" 279 | }, 280 | "require-dev": { 281 | "phpstan/phpstan": "^0.12.63" 282 | }, 283 | "type": "library", 284 | "extra": { 285 | "branch-alias": { 286 | "dev-master": "1.x-dev" 287 | } 288 | }, 289 | "autoload": { 290 | "psr-4": { 291 | "Seld\\CliPrompt\\": "src/" 292 | } 293 | }, 294 | "notification-url": "https://packagist.org/downloads/", 295 | "license": [ 296 | "MIT" 297 | ], 298 | "authors": [ 299 | { 300 | "name": "Jordi Boggiano", 301 | "email": "j.boggiano@seld.be" 302 | } 303 | ], 304 | "description": "Allows you to prompt for user input on the command line, and optionally hide the characters they type", 305 | "keywords": [ 306 | "cli", 307 | "console", 308 | "hidden", 309 | "input", 310 | "prompt" 311 | ], 312 | "support": { 313 | "issues": "https://github.com/Seldaek/cli-prompt/issues", 314 | "source": "https://github.com/Seldaek/cli-prompt/tree/1.0.4" 315 | }, 316 | "time": "2020-12-15T21:32:01+00:00" 317 | } 318 | ], 319 | "packages-dev": [], 320 | "aliases": [], 321 | "minimum-stability": "stable", 322 | "stability-flags": { 323 | "phpgl/ide-stubs": 20, 324 | "phpgl/visu": 20 325 | }, 326 | "prefer-stable": false, 327 | "prefer-lowest": false, 328 | "platform": [], 329 | "platform-dev": [], 330 | "plugin-api-version": "2.3.0" 331 | } 332 | -------------------------------------------------------------------------------- /resources/background/seamlessbg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phpgl/flappyphpant/764e716a108042d8c1930e6625ad49a6b5dd888c/resources/background/seamlessbg.png -------------------------------------------------------------------------------- /resources/shader/background.frag.glsl: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | 3 | in vec2 v_uv; 4 | out vec4 fragment_color; 5 | uniform sampler2D u_texture; 6 | 7 | // a uv modifier to move the background 8 | uniform float bgmove; 9 | 10 | void main() { 11 | vec2 uv = vec2(v_uv.x, v_uv.y); 12 | uv.x += bgmove; 13 | fragment_color = texture(u_texture, uv); 14 | } -------------------------------------------------------------------------------- /resources/shader/background.vert.glsl: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | 3 | layout (location = 0) in vec3 a_pos; 4 | layout (location = 1) in vec2 a_uv; 5 | 6 | out vec2 v_uv; 7 | 8 | uniform mat4 u_view; 9 | uniform mat4 u_projection; 10 | uniform mat4 u_model; 11 | 12 | void main() { 13 | v_uv = a_uv; // pass the texture coordinates to the fragment shader 14 | gl_Position = u_projection * u_view * u_model * vec4(a_pos, 1.0); 15 | } -------------------------------------------------------------------------------- /resources/shader/example_image.frag.glsl: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | 3 | in vec2 v_uv; 4 | in float frame; 5 | 6 | uniform sampler2D u_sprite; 7 | uniform int u_rows; 8 | uniform int u_cols; 9 | 10 | out vec4 fragment_color; 11 | 12 | void main() 13 | { 14 | int row_frame_count = u_rows; 15 | int col_frame_count = u_cols; 16 | 17 | vec2 frame_size = vec2(1.0 / row_frame_count, 1.0 / col_frame_count); 18 | 19 | // determine the position of the current 20 | int frameX = int(mod(frame, row_frame_count)); 21 | int frameY = int(frame / row_frame_count); 22 | vec2 frame_coords = vec2(frameX * frame_size.x, frameY * frame_size.y); 23 | 24 | // calculate the uv 25 | vec2 uv = frame_coords + v_uv * frame_size; 26 | 27 | fragment_color = texture(u_sprite, uv); 28 | } -------------------------------------------------------------------------------- /resources/shader/example_image.vert.glsl: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | 3 | layout (location = 0) in vec2 a_pos; 4 | layout (location = 1) in vec2 a_uv; 5 | layout (location = 2) in mat4 a_model; 6 | layout (location = 6) in float a_frame; 7 | 8 | uniform mat4 u_view; 9 | uniform mat4 u_projection; 10 | uniform vec2 u_resolution; 11 | 12 | out vec2 v_uv; 13 | out float frame; 14 | 15 | void main() 16 | { 17 | // forward the uv and frame to the fragment shader 18 | v_uv = a_uv; 19 | frame = a_frame; 20 | 21 | // calculate the final screenspace position 22 | gl_Position = u_projection * u_view * a_model * vec4(a_pos, 0.0, 1.0); 23 | } -------------------------------------------------------------------------------- /resources/sprites/helpium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phpgl/flappyphpant/764e716a108042d8c1930e6625ad49a6b5dd888c/resources/sprites/helpium.png -------------------------------------------------------------------------------- /resources/sprites/pipe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phpgl/flappyphpant/764e716a108042d8c1930e6625ad49a6b5dd888c/resources/sprites/pipe.png -------------------------------------------------------------------------------- /resources/sprites/visuphpant.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phpgl/flappyphpant/764e716a108042d8c1930e6625ad49a6b5dd888c/resources/sprites/visuphpant.png -------------------------------------------------------------------------------- /resources/sprites/visuphpant2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phpgl/flappyphpant/764e716a108042d8c1930e6625ad49a6b5dd888c/resources/sprites/visuphpant2.png -------------------------------------------------------------------------------- /resources/sprites/visuphpant3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phpgl/flappyphpant/764e716a108042d8c1930e6625ad49a6b5dd888c/resources/sprites/visuphpant3.png -------------------------------------------------------------------------------- /src/Component/GameCamera2DComponent.php: -------------------------------------------------------------------------------- 1 | focusPoint = new Vec2(0.0); 30 | $this->focusPointVelocity = new Vec2(0.0); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Component/GlobalStateComponent.php: -------------------------------------------------------------------------------- 1 | position = new Vec2(0, 0); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Component/SpriteComponent.php: -------------------------------------------------------------------------------- 1 | 46 | */ 47 | private array $rows = []; 48 | 49 | /** 50 | * Toggles debug overlay 51 | */ 52 | public bool $enabled = true; 53 | 54 | /** 55 | * Constructor 56 | * 57 | * As this is a debugging utility, we will use the container directly 58 | */ 59 | public function __construct( 60 | private GameContainer $container 61 | ) { 62 | $this->debugTextRenderer = new DebugOverlayTextRenderer( 63 | $container->resolveGL(), 64 | DebugOverlayTextRenderer::loadDebugFontAtlas(), 65 | ); 66 | 67 | // listen to keyboard events to toggle debug overlay 68 | $container->resolveVisuDispatcher()->register('input.key', function(KeySignal $keySignal) { 69 | if ($keySignal->key == Key::F1 && $keySignal->action == Input::PRESS) { 70 | $this->enabled = !$this->enabled; 71 | } 72 | }); 73 | } 74 | 75 | private function gameLoopMetrics(float $deltaTime) : string 76 | { 77 | $gameLoop = $this->container->resolveGameLoopMain(); 78 | 79 | $row = str_pad("FPS: " . round($gameLoop->getAverageFps()), 8); 80 | $row .= str_pad(" | TC: " . sprintf("%.2f", $gameLoop->getAverageTickCount()), 10); 81 | $row .= str_pad(" | UT: " . $gameLoop->getAverageTickTimeFormatted(), 18); 82 | $row .= str_pad(" | FT: " . $gameLoop->getAverageFrameTimeFormatted(), 16); 83 | $row .= " | delta: " . sprintf("%.4f", $deltaTime); 84 | 85 | return $row; 86 | } 87 | 88 | private function formatNStoHuman(int $ns) : string 89 | { 90 | if ($ns < 1000) { 91 | return sprintf("%.2f ns", $ns); 92 | } 93 | elseif ($ns < 1000000) { 94 | return sprintf("%.2f µs", $ns / 1000); 95 | } 96 | elseif ($ns < 1000000000) { 97 | return sprintf("%.2f ms", $ns / 1000000); 98 | } 99 | else { 100 | return sprintf("%.2f s", $ns / 1000000000); 101 | } 102 | } 103 | 104 | private function gameProfilerMetrics() : array 105 | { 106 | $profiler = $this->container->resolveProfiler(); 107 | 108 | $scopeAverages = $profiler->getAveragesPerScope(); 109 | $rows = []; 110 | 111 | // sort the averages by GPU consumption 112 | uasort($scopeAverages, function($a, $b) { 113 | return $b['gpu'] <=> $a['gpu']; 114 | }); 115 | 116 | foreach($scopeAverages as $scope => $averages) { 117 | $row = str_pad("[" . $scope . ']', 25); 118 | $row .= str_pad(" CPU(" . $averages['cpu_samples'] . "): " . $this->formatNStoHuman((int) $averages['cpu']), 20); 119 | $row .= str_pad(" | GPU(" . $averages['gpu_samples'] . "): " . $this->formatNStoHuman((int) $averages['gpu']), 20); 120 | $row .= str_pad(" | Tri: " . round($averages['gpu_triangles']), 10); 121 | $rows[] = $row; 122 | } 123 | 124 | return $rows; 125 | } 126 | 127 | /** 128 | * Draws the debug text overlay if enabled 129 | */ 130 | public function attachPass(RenderPipeline $pipeline, PipelineResources $resources, RenderTargetResource $rt, float $compensation) 131 | { 132 | // we sync the profile enabled state with the debug overlay 133 | $this->container->resolveProfiler()->enabled = $this->enabled; 134 | 135 | if (!$this->enabled) { 136 | $this->rows = []; // reset the rows to avoid them stacking up 137 | self::$globalRows = []; 138 | return; 139 | } 140 | 141 | // get the actual rendering target 142 | $target = $resources->getRenderTarget($rt); 143 | 144 | // Add current FPS plus the average tick count and the compensation 145 | $this->rows[] = $this->gameLoopMetrics($compensation); 146 | $this->rows[] = "Scene: " . $this->container->resolveGame()->getCurrentScene()->getName() . 147 | ' | Press CTRL + C to open the console'; 148 | 149 | // add global rows 150 | $this->rows = array_merge($this->rows, self::$globalRows); 151 | 152 | 153 | // we render to the backbuffer 154 | $this->debugTextRenderer->attachPass($pipeline, $rt, [ 155 | new DebugOverlayText(implode("\n", $this->rows), 10, 10) 156 | ]); 157 | 158 | $profilerLines = $this->gameProfilerMetrics(); 159 | $y = $rt->height - (count($profilerLines) * $this->debugTextRenderer->lineHeight * $target->contentScaleX); 160 | $y -= 25; 161 | $this->debugTextRenderer->attachPass($pipeline, $rt, [ 162 | new DebugOverlayText(implode("\n", $profilerLines), 10, $y, new Vec3(0.726, 0.865, 1.0)), 163 | ]); 164 | 165 | 166 | // clear the rows for the next frame 167 | $this->rows = []; 168 | self::$globalRows = []; 169 | } 170 | 171 | public function __debugInfo() 172 | { 173 | return [ 174 | 'enabled' => $this->enabled, 175 | 'rows' => $this->rows, 176 | ]; 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/Game.php: -------------------------------------------------------------------------------- 1 | container = $container; 95 | $this->window = $container->resolveWindowMain(); 96 | 97 | // initialize the window 98 | $this->window->initailize($this->container->resolveGL()); 99 | 100 | // enable vsync by default 101 | $this->window->setSwapInterval(1); 102 | 103 | // make the input the windows event handler 104 | $this->window->setEventHandler($this->container->resolveInput()); 105 | 106 | // initialize the input context map 107 | $this->inputContext = $this->container->resolveInputContext(); 108 | 109 | // initialize the pipeline resources 110 | $this->pipelineResources = new PipelineResources($container->resolveGL()); 111 | 112 | // preload all shaders 113 | $container->resolveShaders()->loadAll(function($name, ShaderProgram $shader) { 114 | Logger::info("(shader) loaded: {$name} -> {$shader->id}"); 115 | }); 116 | 117 | // initialize the debug renderers 118 | $this->dbgText = new DebugTextOverlay($container); 119 | $this->dbgConsole = new DebugConsole($container->resolveGL(), $container->resolveInput(), $container->resolveVisuDispatcher()); 120 | $this->dbg3D = new Debug3DRenderer($container->resolveGL()); 121 | Debug3DRenderer::setGlobalInstance($this->dbg3D); 122 | 123 | // load an inital scene 124 | $this->currentScene = new GameViewScene($container); 125 | 126 | // listen for scene switch signals 127 | $this->container->resolveVisuDispatcher()->register('scene.switch', function(SwitchToSceneSignal $signal) { 128 | $this->switchScene($signal->newScene); 129 | }); 130 | } 131 | 132 | /** 133 | * Returns the current scene 134 | */ 135 | public function getCurrentScene() : BaseScene 136 | { 137 | return $this->currentScene; 138 | } 139 | 140 | /** 141 | * Switches to the given scene 142 | */ 143 | public function switchScene(BaseScene $scene) : void 144 | { 145 | $this->nextScene = $scene; 146 | } 147 | 148 | /** 149 | * Start the game 150 | * This will begin the game loop 151 | */ 152 | public function start() 153 | { 154 | // initialize the current scene 155 | $this->currentScene->load(); 156 | 157 | // start the game loop 158 | $this->container->resolveGameLoopMain()->start(); 159 | } 160 | 161 | /** 162 | * Update the games state 163 | * This method might be called multiple times per frame, or not at all if 164 | * the frame rate is very high. 165 | * 166 | * The update method should step the game forward in time, this is the place 167 | * where you would update the position of your game objects, check for collisions 168 | * and so on. 169 | * 170 | * @return void 171 | */ 172 | public function update() : void 173 | { 174 | // reset the input context for the next tick 175 | $this->inputContext->reset(); 176 | 177 | // poll for new events 178 | $this->window->pollEvents(); 179 | 180 | // update the current scene 181 | $this->currentScene->update(); 182 | } 183 | 184 | /** 185 | * Render the current game state 186 | * This method is called once per frame. 187 | * 188 | * The render method should draw the current game state to the screen. You recieve 189 | * a delta time value which you can use to interpolate between the current and the 190 | * previous frame. This is useful for animations and other things that should be 191 | * smooth with variable frame rates. 192 | * 193 | * @param float $deltaTime 194 | * @return void 195 | */ 196 | public function render(float $deltaTime) : void 197 | { 198 | $windowRenderTarget = $this->window->getRenderTarget(); 199 | 200 | $data = new PipelineContainer; 201 | $pipeline = new RenderPipeline($this->pipelineResources, $data, $windowRenderTarget); 202 | $context = new RenderContext($pipeline, $data, $this->pipelineResources, $deltaTime); 203 | 204 | // backbuffer render target 205 | $backbuffer = $data->get(BackbufferData::class)->target; 206 | 207 | // render the current scene 208 | $this->currentScene->render($context); 209 | 210 | // render debug text 211 | $this->dbg3D->attachPass($pipeline, $backbuffer); 212 | $this->dbgText->attachPass($pipeline, $this->pipelineResources, $backbuffer, $deltaTime); 213 | $this->dbgConsole->attachPass($pipeline, $this->pipelineResources, $backbuffer); 214 | 215 | // execute the pipeline 216 | $pipeline->execute($this->frameIndex++, $this->container->resolveProfiler()); 217 | 218 | // finalize the profiler 219 | $this->container->resolveProfiler()->finalize(); 220 | 221 | $this->window->swapBuffers(); 222 | 223 | // switch to the next scene if requested 224 | if ($this->nextScene) { 225 | Logger::info("Switching to scene: {$this->nextScene->getName()}"); 226 | $this->currentScene->unload(); 227 | $this->currentScene = $this->nextScene; 228 | $this->currentScene->load(); 229 | $this->nextScene = null; 230 | } 231 | } 232 | 233 | /** 234 | * Loop should stop 235 | * This method is called once per frame and should return true if the game loop 236 | * should stop. This is useful if you want to quit the game after a certain amount 237 | * of time or if the player has lost all his lives etc.. 238 | * 239 | * @return bool 240 | */ 241 | public function shouldStop() : bool 242 | { 243 | return $this->window->shouldClose(); 244 | } 245 | 246 | /** 247 | * Custom debug info. 248 | * I know, I know, there should't be references to game in the first place.. 249 | */ 250 | public function __debugInfo() { 251 | return ['currentScene' => $this->currentScene->getName()]; 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /src/Pass/SpritePass.php: -------------------------------------------------------------------------------- 1 | $exampleImages 26 | * @param array $spriteTextures 27 | * @param array $spriteDimensions 28 | */ 29 | public function __construct( 30 | private GLState $gl, 31 | private ShaderProgram $shader, 32 | private array $spriteTextures, 33 | private array $spriteDimensions, 34 | private BasicInstancedVertexArray $vertexArray, 35 | private RenderTargetResource $renderTarget, 36 | private EntitiesInterface $entities, 37 | ) { 38 | } 39 | 40 | /** 41 | * Executes the render pass 42 | */ 43 | public function setup(RenderPipeline $pipeline, PipelineContainer $data): void 44 | { 45 | } 46 | 47 | /** 48 | * Executes the render pass 49 | */ 50 | public function execute(PipelineContainer $data, PipelineResources $resources): void 51 | { 52 | $resources->activateRenderTarget($this->renderTarget); 53 | 54 | $cameraData = $data->get(CameraData::class); 55 | 56 | $this->shader->use(); 57 | 58 | $this->shader->setUniformMat4('u_view', false, $cameraData->view); 59 | $this->shader->setUniformMat4('u_projection', false, $cameraData->projection); 60 | $this->shader->setUniformVec2('u_resolution', $cameraData->getResolutionVec()); 61 | $this->shader->setUniform1i('u_sprite', 0); 62 | 63 | // group the sprites by their sprite name 64 | $groupedSprites = []; 65 | foreach ($this->entities->view(SpriteComponent::class) as $entity => $sprite) { 66 | if (!isset($groupedSprites[$sprite->spriteName])) { 67 | $groupedSprites[$sprite->spriteName] = []; 68 | } 69 | $groupedSprites[$sprite->spriteName][] = $entity; 70 | } 71 | 72 | glEnable(GL_BLEND); 73 | glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); 74 | glDisable(GL_DEPTH_TEST); 75 | glDisable(GL_CULL_FACE); 76 | 77 | 78 | $this->vertexArray->bind(); 79 | 80 | $buffer = new FloatBuffer(); 81 | foreach($groupedSprites as $spriteName => $sprites) 82 | { 83 | if (!isset($this->spriteTextures[$spriteName])) { 84 | throw new \Exception("Sprite texture not found: {$spriteName}"); 85 | } 86 | 87 | $this->spriteTextures[$spriteName]->bind(GL_TEXTURE0); 88 | $this->shader->setUniform1i('u_rows', $this->spriteTextures[$spriteName]->width() / $this->spriteDimensions[$spriteName]->x); 89 | $this->shader->setUniform1i('u_cols', $this->spriteTextures[$spriteName]->height() / $this->spriteDimensions[$spriteName]->y); 90 | $buffer->clear(); 91 | 92 | foreach ($sprites as $entity) 93 | { 94 | $transform = $this->entities->get($entity, Transform::class); 95 | $sprite = $this->entities->get($entity, SpriteComponent::class); 96 | $buffer->pushMat4($transform->getInterpolatedLocalMatrix($cameraData->compensation)); 97 | $buffer->push($sprite->spriteFrame); 98 | } 99 | 100 | $this->vertexArray->uploadInstanceData($buffer); 101 | $this->vertexArray->drawAll(GL_TRIANGLE_STRIP); 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/Renderer/BackgroundRenderer.php: -------------------------------------------------------------------------------- 1 | backgroundShader = $this->shaders->get('background'); 52 | 53 | // load the background texture 54 | // this is pixel artish so we want to use nearest neighbor filtering 55 | $backgroundOptions = new TextureOptions; 56 | $backgroundOptions->minFilter = GL_NEAREST; 57 | $backgroundOptions->magFilter = GL_NEAREST; 58 | $this->backgroundTexture = new Texture($gl, 'background'); 59 | $this->backgroundTexture->loadFromFile(VISU_PATH_RESOURCES . '/background/seamlessbg.png', $backgroundOptions); 60 | 61 | // create the vertex array 62 | $this->backgroundVA = new QuadVertexArray($gl); 63 | } 64 | 65 | /** 66 | * Attaches a render pass to the pipeline 67 | * 68 | * @param RenderPipeline $pipeline 69 | * @param RenderTargetResource $renderTarget 70 | * @param array $exampleImages 71 | */ 72 | public function attachPass( 73 | RenderPipeline $pipeline, 74 | RenderTargetResource $renderTarget, 75 | EntitiesInterface $entities, 76 | ) : void 77 | { 78 | // you do not always have to create a new class for a render pass 79 | // often its more convenient to just create a closure as showcased here 80 | // to render the background 81 | $pipeline->addPass(new CallbackPass( 82 | 'BackgroundPass', 83 | // setup (we need to declare who is reading and writing what) 84 | function(RenderPass $pass, RenderPipeline $pipeline, PipelineContainer $data) use($renderTarget) { 85 | $pipeline->writes($pass, $renderTarget); 86 | }, 87 | // execute 88 | function(PipelineContainer $data, PipelineResources $resources) use($renderTarget, $entities) 89 | { 90 | $playerEntity = $entities->firstWith(PlayerComponent::class); 91 | $playerTransform = $entities->get($playerEntity, Transform::class); 92 | 93 | $resources->activateRenderTarget($renderTarget); 94 | 95 | glDisable(GL_DEPTH_TEST); 96 | glDisable(GL_CULL_FACE); 97 | 98 | $cameraData = $data->get(CameraData::class); 99 | 100 | 101 | // create a copy of the view matrix and remove the translation 102 | // because we want a parallax effect 103 | 104 | $view = $cameraData->view->copy(); 105 | $view[12] = 0.0; 106 | $view[13] = 0.0; 107 | $view[14] = 0.0; 108 | // $view->translate($cameraData->renderCamera->transform->position * -1 * $scale); 109 | 110 | // enable our shader and set the uniforms camera uniforms 111 | $this->backgroundShader->use(); 112 | $this->backgroundShader->setUniformMat4('u_view', false, $view); 113 | $this->backgroundShader->setUniformMat4('u_projection', false, $cameraData->projection); 114 | $this->backgroundShader->setUniform1i('u_texture', 0); 115 | 116 | // bind the texture 117 | $this->backgroundTexture->bind(GL_TEXTURE0); 118 | 119 | // draw the quad to fill the screen 120 | $aspectRatio = $this->backgroundTexture->width() / $this->backgroundTexture->height(); 121 | 122 | $height = $cameraData->viewport->getHeight(); 123 | $width = $height * $aspectRatio; 124 | 125 | $transform = new Transform; 126 | $transform->scale->x = $width * 0.5; 127 | $transform->scale->y = $height * 0.5; 128 | $transform->position->x = 0; 129 | $transform->position->y = 0; 130 | $this->backgroundShader->setUniformMat4('u_model', false, $transform->getLocalMatrix()); 131 | 132 | // determine the background movement based 133 | $this->backgroundShader->setUniform1f('bgmove', $playerTransform->position->x * 0.0003); 134 | 135 | 136 | $this->backgroundVA->draw(); 137 | } 138 | )); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/Renderer/SpriteRenderer.php: -------------------------------------------------------------------------------- 1 | 29 | */ 30 | private array $spriteTextures = []; 31 | 32 | /** 33 | * The loaded sprite dimensions 34 | * 35 | * @var array 36 | */ 37 | private array $spriteDimensions = []; 38 | 39 | /** 40 | * Vertex array used for rendering 41 | */ 42 | private BasicInstancedVertexArray $vertexArray; 43 | 44 | public function __construct( 45 | private GLState $gl, 46 | private ShaderCollection $shaders, 47 | ) 48 | { 49 | // create the shader program 50 | $this->imageShader = $this->shaders->get('example_image'); 51 | 52 | // this is pixel artish so we want to use nearest neighbor filtering 53 | $backgroundOptions = new TextureOptions; 54 | $backgroundOptions->minFilter = GL_NEAREST; 55 | $backgroundOptions->magFilter = GL_NEAREST; 56 | 57 | foreach([ 58 | 'visuphpant.png' => new Vec2(32, 32), 59 | 'visuphpant2.png' => new Vec2(32, 32), 60 | 'visuphpant3.png' => new Vec2(32, 32), 61 | 'helpium.png' => new Vec2(32, 32), 62 | 'pipe.png' => new Vec2(64, 64), 63 | ] as $spriteName => $dimensions) { 64 | $texture = new Texture($gl, $spriteName); 65 | $texture->loadFromFile(VISU_PATH_RESOURCES . '/sprites/' . $spriteName, $backgroundOptions); 66 | 67 | $this->bindSprite($spriteName, $texture, $dimensions); 68 | } 69 | 70 | // create a vertex array for rendering 71 | $this->vertexArray = new BasicInstancedVertexArray($gl, [2, 2], [4, 4, 4, 4, 1]); 72 | $this->vertexArray->uploadVertexData(new FloatBuffer([ 73 | // vertex positions 74 | // this makes up the quad 75 | -1.0, -1.0, 0.0, -1.0, 76 | 1.0, -1.0, 1.0, -1.0, 77 | -1.0, 1.0, 0.0, 0.0, 78 | 1.0, 1.0, 1.0, 0.0, 79 | ])); 80 | } 81 | 82 | /** 83 | * Attaches a render pass to the pipeline 84 | * 85 | * @param RenderPipeline $pipeline 86 | * @param RenderTargetResource $renderTarget 87 | * @param array $exampleImages 88 | */ 89 | public function attachPass( 90 | RenderPipeline $pipeline, 91 | RenderTargetResource $renderTarget, 92 | EntitiesInterface $entities, 93 | ) : void 94 | { 95 | $pipeline->addPass(new SpritePass( 96 | $this->gl, 97 | $this->imageShader, 98 | $this->spriteTextures, 99 | $this->spriteDimensions, 100 | $this->vertexArray, 101 | $renderTarget, 102 | $entities 103 | )); 104 | } 105 | 106 | /** 107 | * Bind a sprite to the renderer 108 | */ 109 | public function bindSprite(string $name, Texture $texture, Vec2 $dimensions) : void 110 | { 111 | $this->spriteTextures[$name] = $texture; 112 | $this->spriteDimensions[$name] = $dimensions; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/Scene/BaseScene.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | protected array $systems = []; 24 | 25 | /** 26 | * Constructor 27 | * 28 | * Dont load resources or bind event listeners here, use the `load` method instead. 29 | * We want to be able to load scenes without loading their resources. For example 30 | * when preparing a scene to be switched or a loading screen. 31 | */ 32 | public function __construct( 33 | protected GameContainer $container, 34 | ) 35 | { 36 | $this->entities = new EntityRegisty(); 37 | } 38 | 39 | /** 40 | * Destructor 41 | */ 42 | public function __destruct() 43 | { 44 | $this->unregisterSystems(); 45 | } 46 | 47 | /** 48 | * Returns the scenes name 49 | */ 50 | abstract public function getName() : string; 51 | 52 | /** 53 | * Binds an array of systems to the scene 54 | * This is handy as you can register and unregister all thsese systems at once 55 | * 56 | * Why do we not just call update and render on all systems automatically? 57 | * Because we want to be able to control the order in which systems are updated and rendered, 58 | * in some cases we don't want to update all systems every frame. 59 | * This is really dependant on the game you are making. Which is why VISU doesn't come with a BaseScene class. 60 | * 61 | * @param array $systems 62 | */ 63 | protected function bindSystems(array $systems) : void 64 | { 65 | $this->systems = array_merge($this->systems, $systems); 66 | } 67 | 68 | /** 69 | * Registers all binded systems 70 | */ 71 | public function registerSystems() : void 72 | { 73 | foreach ($this->systems as $system) { 74 | $system->register($this->entities); 75 | } 76 | } 77 | 78 | /** 79 | * Unregisters all binded systems 80 | */ 81 | public function unregisterSystems() : void 82 | { 83 | foreach ($this->systems as $system) { 84 | $system->unregister($this->entities); 85 | } 86 | } 87 | 88 | /** 89 | * Loads resources required for the scene, prepere base entities 90 | * basically prepare anything that is required to be ready before the scene is rendered. 91 | * 92 | * @return void 93 | */ 94 | abstract public function load() : void; 95 | 96 | /** 97 | * Unloads resources required for the scene, cleanup base entities 98 | * 99 | * @return void 100 | */ 101 | public function unload() : void 102 | { 103 | $this->unregisterSystems(); 104 | } 105 | 106 | /** 107 | * Updates the scene state 108 | * This is the place where you should update your game state. 109 | * 110 | * @return void 111 | */ 112 | abstract public function update() : void; 113 | 114 | /** 115 | * Renders the scene 116 | * This is the place where you should render your game state. 117 | * 118 | * @param RenderContext $context 119 | */ 120 | abstract public function render(RenderContext $context) : void; 121 | } 122 | -------------------------------------------------------------------------------- /src/Scene/GameViewScene.php: -------------------------------------------------------------------------------- 1 | cameraSystem = new CameraSystem2D( 78 | $this->container->resolveInput(), 79 | $this->container->resolveVisuDispatcher(), 80 | $this->container->resolveInputContext(), 81 | ); 82 | 83 | // basic rendering system 84 | $this->renderingSystem = new RenderingSystem2D( 85 | $this->container->resolveGL(), 86 | $this->container->resolveShaders() 87 | ); 88 | 89 | // the thing moving the flying phpants 90 | $this->visuPhpantSystem = new FlappyPHPantSystem( 91 | $this->container->resolveInputContext() 92 | ); 93 | 94 | // the pipes 95 | $this->pipeSystem = new PipeSystem(); 96 | 97 | // bind all systems to the scene itself 98 | $this->bindSystems([ 99 | $this->cameraSystem, 100 | $this->renderingSystem, 101 | $this->visuPhpantSystem, 102 | $this->pipeSystem, 103 | ]); 104 | } 105 | 106 | /** 107 | * Destructor 108 | */ 109 | public function __destruct() 110 | { 111 | $this->unload(); 112 | } 113 | 114 | /** 115 | * Registers the console commmand handler, for level scene specific commands 116 | */ 117 | private function registerConsoleCommands() 118 | { 119 | $this->consoleHandlerId = $this->container->resolveVisuDispatcher()->register(DebugConsole::EVENT_CONSOLE_COMMAND, function(ConsoleCommandSignal $signal) 120 | { 121 | // do something with the console command (if you want to) 122 | var_dump($signal->commandParts); 123 | }); 124 | } 125 | 126 | /** 127 | * Loads resources required for the scene, prepere base entiteis 128 | * basically prepare anything that is required to be ready before the scene is rendered. 129 | * 130 | * @return void 131 | */ 132 | public function load(): void 133 | { 134 | // register console command 135 | $this->registerConsoleCommands(); 136 | 137 | // register key handler for debugging 138 | // usally a system should handle this but this is temporary 139 | $this->keyboardHandlerId = $this->container->resolveVisuDispatcher()->register('input.key', function(KeySignal $signal) { 140 | $this->handleKeyboardEvent($signal); 141 | }); 142 | 143 | // create a glboal state singleton 144 | $globalStateComponent = new GlobalStateComponent; 145 | // read the highscore from disk if it exists 146 | if (file_exists(VISU_PATH_CACHE . '/highscore.txt')) { 147 | $globalStateComponent->highScore = (int) file_get_contents(VISU_PATH_CACHE . '/highscore.txt'); 148 | } 149 | $this->entities->setSingleton($globalStateComponent); 150 | 151 | // register the systems 152 | $this->registerSystems(); 153 | } 154 | 155 | /** 156 | * Unloads resources required for the scene, cleanup base entities 157 | * 158 | * @return void 159 | */ 160 | public function unload(): void 161 | { 162 | // write the highscore to disk 163 | $gameState = $this->entities->getSingleton(GlobalStateComponent::class); 164 | file_put_contents(VISU_PATH_CACHE . '/highscore.txt', (string) $gameState->highScore); 165 | 166 | parent::unload(); 167 | $this->container->resolveVisuDispatcher()->unregister('input.key', $this->keyboardHandlerId); 168 | $this->container->resolveVisuDispatcher()->unregister(DebugConsole::EVENT_CONSOLE_COMMAND, $this->consoleHandlerId); 169 | } 170 | 171 | /** 172 | * Updates the scene state 173 | * This is the place where you should update your game state. 174 | * 175 | * @return void 176 | */ 177 | public function update(): void 178 | { 179 | $gameState = $this->entities->getSingleton(GlobalStateComponent::class); 180 | 181 | // count ticks while the game is not paused 182 | if (!$gameState->paused) { 183 | $gameState->tick++; 184 | } 185 | 186 | $this->cameraSystem->update($this->entities); 187 | $this->visuPhpantSystem->update($this->entities); 188 | $this->pipeSystem->update($this->entities); 189 | 190 | // update the rendering system 191 | $this->renderingSystem->update($this->entities); 192 | } 193 | 194 | /** 195 | * Renders the scene 196 | * This is the place where you should render your game state. 197 | * 198 | * @param RenderContext $context 199 | */ 200 | public function render(RenderContext $context): void 201 | { 202 | // output some general game view stats 203 | $gameState = $this->entities->getSingleton(GlobalStateComponent::class); 204 | DebugTextOverlay::debugString(sprintf('Score: %d, tick: %d', $gameState->score, $gameState->tick)); 205 | 206 | // update the camera 207 | $this->cameraSystem->render($this->entities, $context); 208 | 209 | // pipe system needs to adjust the pipes to the camera 210 | $this->pipeSystem->render($this->entities, $context); 211 | 212 | // let the rendering system render the scene 213 | $this->renderingSystem->render($this->entities, $context); 214 | } 215 | 216 | /** 217 | * Keyboard event handler 218 | */ 219 | public function handleKeyboardEvent(KeySignal $signal): void 220 | { 221 | // reload the scene by switching to the same scene 222 | if ($signal->key === Key::F5 && $signal->action === Input::PRESS) { 223 | Logger::info('Reloading scene'); 224 | $this->container->resolveVisuDispatcher()->dispatch('scene.switch', new SwitchToSceneSignal(new GameViewScene($this->container))); 225 | } 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /src/SignalHandler/WindowActionsHandler.php: -------------------------------------------------------------------------------- 1 | key == GLFW_KEY_ESCAPE && $signal->action == GLFW_PRESS) { 20 | $signal->window->setShouldClose(true); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Signals/ResetGameSignal.php: -------------------------------------------------------------------------------- 1 | registerComponent(PlayerComponent::class); 51 | 52 | // create an inital camera entity 53 | $cameraEntity = $entities->create(); 54 | $camera = $entities->attach($cameraEntity, new Camera(CameraProjectionMode::orthographicStaticWorld)); 55 | $camera->nearPlane = -10; 56 | $camera->farPlane = 10; 57 | 58 | // make the camera the active camera 59 | $this->setActiveCameraEntity($cameraEntity); 60 | } 61 | 62 | /** 63 | * Unregisters the system, this is where you can handle any cleanup. 64 | * 65 | * @return void 66 | */ 67 | public function unregister(EntitiesInterface $entities) : void 68 | { 69 | parent::unregister($entities); 70 | } 71 | 72 | /** 73 | * Override this method to handle the cursor position in game mode 74 | * 75 | * @param CursorPosSignal $signal 76 | * @return void 77 | */ 78 | protected function handleCursorPosVISUGame(EntitiesInterface $entities, CursorPosSignal $signal) : void 79 | { 80 | // handle mouse movement 81 | } 82 | 83 | /** 84 | * Override this method to handle the scroll wheel in game mode 85 | * 86 | * @param ScrollSignal $signal 87 | * @return void 88 | */ 89 | protected function handleScrollVISUGame(EntitiesInterface $entities, ScrollSignal $signal) : void 90 | { 91 | // handle mouse scroll 92 | } 93 | 94 | /** 95 | * Override this method to update the camera in game mode 96 | * 97 | * @param EntitiesInterface $entities 98 | */ 99 | public function updateGameCamera(EntitiesInterface $entities, Camera $camera) : void 100 | { 101 | $playerEntity = $entities->firstWith(PlayerComponent::class); 102 | $playerTransform = $entities->get($playerEntity, Transform::class); 103 | 104 | // copy the player position to the camera 105 | $camera->transform->markDirty(); 106 | $camera->transform->position = new Vec3( 107 | $playerTransform->position->x, // camera always follows the player 108 | 0, // camera is always in the vertical center of the world 109 | 0 110 | ); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/System/FlappyPHPantSystem.php: -------------------------------------------------------------------------------- 1 | registerComponent(SpriteComponent::class); 45 | $entities->registerComponent(PlayerComponent::class); 46 | $entities->registerComponent(DynamicTextLabelComponent::class); 47 | 48 | $this->setupPlayerEntity($entities, $entities->create()); 49 | $this->setupScoreLabel($entities); 50 | } 51 | 52 | /** 53 | * Create the requried components for the player entity 54 | */ 55 | private function setupPlayerEntity(EntitiesInterface $entities, int $playerEntity) : void 56 | { 57 | $gameState = $entities->getSingleton(GlobalStateComponent::class); 58 | 59 | // remove the components if they exist 60 | $entities->detachAll($playerEntity); 61 | 62 | $entities->attach($playerEntity, new SpriteComponent($gameState->playerSprite)); 63 | $entities->attach($playerEntity, new PlayerComponent); 64 | $transform = $entities->attach($playerEntity, new Transform); 65 | $transform->position = new Vec3(0, 0, 0); 66 | $transform->scale = new Vec3(-7, 7, 1); 67 | } 68 | 69 | /** 70 | * Create the score label 71 | */ 72 | private function setupScoreLabel(EntitiesInterface $entities) : void 73 | { 74 | $this->scoreLabelEntity = $entities->create(); 75 | $entities->attach($this->scoreLabelEntity, new DynamicTextLabelComponent( 76 | text: '', 77 | isStatic: true, 78 | )); 79 | $transform = $entities->attach($this->scoreLabelEntity, new Transform); 80 | $transform->position = new Vec3(0, 40, 0); 81 | $transform->scale = new Vec3(0.5); 82 | 83 | $this->highScoreLabelEntity = $entities->create(); 84 | $entities->attach($this->highScoreLabelEntity, new DynamicTextLabelComponent( 85 | text: '', 86 | )); 87 | $transform = $entities->attach($this->highScoreLabelEntity, new Transform); 88 | $transform->position = new Vec3(0, 30, 0); 89 | $transform->scale = new Vec3(0.5); 90 | 91 | // also create a "Press Space to Start" label 92 | $startLabelEntity = $entities->create(); 93 | $entities->attach($startLabelEntity, new DynamicTextLabelComponent( 94 | text: 'Press Space to Start' 95 | )); 96 | $transform = $entities->attach($startLabelEntity, new Transform); 97 | $transform->position = new Vec3(0, -20, 0); 98 | $transform->scale = new Vec3(0.5); 99 | } 100 | 101 | /** 102 | * Unregisters the system, this is where you can handle any cleanup. 103 | * 104 | * @return void 105 | */ 106 | public function unregister(EntitiesInterface $entities) : void 107 | { 108 | } 109 | 110 | /** 111 | * Updates handler, this is where the game state should be updated. 112 | * 113 | * @return void 114 | */ 115 | public function update(EntitiesInterface $entities) : void 116 | { 117 | $gameState = $entities->getSingleton(GlobalStateComponent::class); 118 | 119 | // update the score label 120 | $scoreLabel = $entities->get($this->scoreLabelEntity, DynamicTextLabelComponent::class); 121 | if ($gameState->score > 0) { 122 | $scoreLabel->text = 'Score: ' . $gameState->score; 123 | } else { 124 | $scoreLabel->text = ''; 125 | } 126 | 127 | // update the highscore label 128 | $highScoreLabel = $entities->get($this->highScoreLabelEntity, DynamicTextLabelComponent::class); 129 | $highScoreLabel->text = 'High Score: ' . $gameState->highScore; 130 | 131 | // waiting for start 132 | if ($gameState->waitingForStart) { 133 | 134 | if ($this->inputContext->actions->didButtonPress('jump')) { 135 | $gameState->waitingForStart = false; 136 | $gameState->paused = false; 137 | } else { 138 | return; 139 | } 140 | } 141 | 142 | $playerEntity = $entities->firstWith(PlayerComponent::class); 143 | $playerComponent = $entities->get($playerEntity, PlayerComponent::class); 144 | $playerTransform = $entities->get($playerEntity, Transform::class); 145 | $playerTransform->storePrevious(); 146 | 147 | // reset the game 148 | if ( 149 | $this->inputContext->actions->didButtonPress('reset') || 150 | ( 151 | $playerComponent->dying && 152 | $this->inputContext->actions->didButtonPress('jump') && 153 | $playerComponent->speed < 0.5 154 | ) 155 | ) { 156 | $gameState->paused = true; 157 | $gameState->waitingForStart = true; 158 | $gameState->tick = 0; 159 | // reset player 160 | $this->setupPlayerEntity($entities, $playerEntity); 161 | return; 162 | } 163 | 164 | if ($gameState->paused) { 165 | return; 166 | } 167 | 168 | // update the player during play 169 | if ($playerComponent->dying) { 170 | $this->updateDuringDeath($entities); 171 | } else { 172 | $this->updateDuringPlay($entities); 173 | } 174 | } 175 | 176 | /** 177 | * Update the player movement during play 178 | */ 179 | public function updateDuringPlay(EntitiesInterface $entities) : void 180 | { 181 | $playerEntity = $entities->firstWith(PlayerComponent::class); 182 | $playerComponent = $entities->get($playerEntity, PlayerComponent::class); 183 | $playerTransform = $entities->get($playerEntity, Transform::class); 184 | $gameState = $entities->getSingleton(GlobalStateComponent::class); 185 | 186 | // count jump tick 187 | $playerComponent->jumpTick++; 188 | 189 | // apply jump 190 | if ($this->inputContext->actions->didButtonPress('jump')) { 191 | $playerComponent->velocity = $playerComponent->jumpForce; 192 | $playerComponent->jumpTick = 0; 193 | } 194 | 195 | // apply gravity 196 | $playerComponent->velocity -= $playerComponent->gravity; 197 | 198 | // apply velocity 199 | $playerComponent->position->x = $playerComponent->position->x + $playerComponent->speed; 200 | $playerComponent->position->y = $playerComponent->position->y + $playerComponent->velocity; 201 | 202 | // copy the player position to the transform 203 | $playerTransform->position->x = $playerComponent->position->x; 204 | $playerTransform->position->y = $playerComponent->position->y; 205 | 206 | // if the player falls through the floor, underneath the first pipe 207 | // we enter the helpium mode 208 | if ($playerTransform->position->y < -80 && $gameState->score == 1) { 209 | $playerComponent->position->y = 45; 210 | $playerComponent->position->x = -30; 211 | $playerComponent->velocity = 0; 212 | 213 | $gameState->playerSprite = 'helpium.png'; 214 | $gameState->alwaysClosingPipes = true; 215 | $gameState->closingPipesDifficulty = 0.3; 216 | $sprite = $entities->get($playerEntity, SpriteComponent::class); 217 | $sprite->spriteName = $gameState->playerSprite; 218 | 219 | } 220 | 221 | // change the displayed sprite frame based on the jump tick 222 | $spriteComponent = $entities->get($playerEntity, SpriteComponent::class); 223 | if ($playerComponent->jumpTick < 8) { 224 | $spriteComponent->spriteFrame = 2; 225 | } else if ($playerComponent->jumpTick < 15) { 226 | $spriteComponent->spriteFrame = 1; 227 | } else { 228 | $spriteComponent->spriteFrame = 0; 229 | } 230 | } 231 | 232 | /** 233 | * Update the player movement during death 234 | */ 235 | public function updateDuringDeath(EntitiesInterface $entities) : void 236 | { 237 | $playerEntity = $entities->firstWith(PlayerComponent::class); 238 | $playerComponent = $entities->get($playerEntity, PlayerComponent::class); 239 | $playerTransform = $entities->get($playerEntity, Transform::class); 240 | $spriteComponent = $entities->get($playerEntity, SpriteComponent::class); 241 | $spriteComponent->spriteFrame = 2; 242 | 243 | $playerComponent->speed = $playerComponent->speed * 0.99; 244 | 245 | $playerTransform->position->x = $playerTransform->position->x - $playerComponent->speed; 246 | $playerTransform->orientation->rotate($playerComponent->speed / 5, new Vec3(0, 0, 1)); 247 | 248 | // apply gravity 249 | $playerComponent->velocity -= $playerComponent->gravity; 250 | 251 | // dampen velocity 252 | $playerComponent->velocity = $playerComponent->velocity * 0.97; 253 | 254 | // apply velocity 255 | $playerTransform->position->y = $playerTransform->position->y + $playerComponent->velocity; 256 | 257 | // floor 258 | if ($playerTransform->position->y < -45) { 259 | $playerTransform->position->y = -45; 260 | $playerComponent->velocity = $playerComponent->velocity * -1; 261 | } 262 | } 263 | 264 | /** 265 | * Handles rendering of the scene, here you can attach additional render passes, 266 | * modify the render pipeline or customize rendering related data. 267 | * 268 | * @param RenderContext $context 269 | */ 270 | public function render(EntitiesInterface $entities, RenderContext $context) : void 271 | { 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /src/System/PipeSystem.php: -------------------------------------------------------------------------------- 1 | 54 | */ 55 | private array $pipeHeights = []; 56 | 57 | /** 58 | * The predetermined pipe gabs 59 | * This determines how large the gab is where the player can fly through 60 | */ 61 | private array $pipeGabs = []; 62 | 63 | /** 64 | * An array of entities designed to hold the pipes 65 | * 66 | * @var array 72 | */ 73 | private array $pipeEntities = []; 74 | 75 | /** 76 | * Generates more pipes when needed. (for the players who are actually pass 1000 pipes or 77 | * the sneaky little cheaters) 78 | */ 79 | private function generateMorePipes() : void 80 | { 81 | // generate more pipes 82 | $ioff = count($this->pipeHeights); 83 | for($i = $ioff; $i < $ioff + 100; $i++) { 84 | $height = mt_rand(-30, 30); 85 | $this->pipeHeights[] = $height; 86 | 87 | // the gab should be smaller based on the distance traveled 88 | $this->pipeGabs[] = max(40.0 - $i * 0.05, 15.0); 89 | } 90 | } 91 | 92 | 93 | /** 94 | * Registers the system, this is where you should register all required components. 95 | * 96 | * @return void 97 | */ 98 | public function register(EntitiesInterface $entities) : void 99 | { 100 | $entities->registerComponent(SpriteComponent::class); 101 | $entities->registerComponent(PlayerComponent::class); 102 | 103 | // precaclulate the level by generating the pipe heights 104 | mt_srand(42); 105 | $this->generateMorePipes(); 106 | } 107 | 108 | /** 109 | * Unregisters the system, this is where you can handle any cleanup. 110 | * 111 | * @return void 112 | */ 113 | public function unregister(EntitiesInterface $entities) : void 114 | { 115 | } 116 | 117 | /** 118 | * Will create and or remove entities to ensure that we have the correct amount of pipes 119 | */ 120 | private function ensurePipeEntities(EntitiesInterface $entities, int $count) : void 121 | { 122 | if (count($this->pipeEntities) < $count) { 123 | for ($i = count($this->pipeEntities); $i < $count; $i++) { 124 | 125 | // pipes 126 | $pipeTopEntity = $entities->create(); 127 | $entities->attach($pipeTopEntity, new Transform); 128 | $pipeTopSprite = $entities->attach($pipeTopEntity, new SpriteComponent('pipe.png')); 129 | $pipeTopSprite->spriteFrame = 0; // the first sprite frame 130 | 131 | $pipeBottomEntity = $entities->create(); 132 | $entities->attach($pipeBottomEntity, new Transform); 133 | $pipeBottomSprite = $entities->attach($pipeBottomEntity, new SpriteComponent('pipe.png')); 134 | $pipeBottomSprite->spriteFrame = 0; 135 | 136 | // outlets 137 | $outletTopEntity = $entities->create(); 138 | $entities->attach($outletTopEntity, new Transform); 139 | $outletTopSprite = $entities->attach($outletTopEntity, new SpriteComponent('pipe.png')); 140 | $outletTopSprite->spriteFrame = 1; // the second sprite frame 141 | 142 | $outletBottomEntity = $entities->create(); 143 | $entities->attach($outletBottomEntity, new Transform); 144 | $outletBottomSprite = $entities->attach($outletBottomEntity, new SpriteComponent('pipe.png')); 145 | $outletBottomSprite->spriteFrame = 1; 146 | 147 | $this->pipeEntities[] = [ 148 | 'pipeTop' => $pipeTopEntity, 149 | 'pipeBottom' => $pipeBottomEntity, 150 | 'outletTop' => $outletTopEntity, 151 | 'outletBottom' => $outletBottomEntity, 152 | ]; 153 | } 154 | } 155 | 156 | elseif (count($this->pipeEntities) > $count) { 157 | for ($i = count($this->pipeEntities) - 1; $i >= $count; $i--) { 158 | $entities->destroy($this->pipeEntities[$i]['pipeTop']); 159 | $entities->destroy($this->pipeEntities[$i]['pipeBottom']); 160 | $entities->destroy($this->pipeEntities[$i]['outletTop']); 161 | $entities->destroy($this->pipeEntities[$i]['outletBottom']); 162 | unset($this->pipeEntities[$i]); 163 | } 164 | } 165 | } 166 | 167 | /** 168 | * Returns the number of pipes required to fill the viewport 169 | */ 170 | public function getPipeCount() : int 171 | { 172 | return (int) ceil($this->viewportWidth / $this->pipeDistance); 173 | } 174 | 175 | /** 176 | * Simple bounce ease out function 177 | */ 178 | function bounceEaseOut(float $alpha) : float { 179 | if ($alpha < (1 / 2.75)) { 180 | return 7.5625 * $alpha * $alpha; 181 | } else if ($alpha < (2 / 2.75)) { 182 | return 7.5625 * ($alpha -= (1.5 / 2.75)) * $alpha + 0.75; 183 | } else if ($alpha < (2.5 / 2.75)) { 184 | return 7.5625 * ($alpha -= (2.25 / 2.75)) * $alpha + 0.9375; 185 | } else { 186 | return 7.5625 * ($alpha -= (2.625 / 2.75)) * $alpha + 0.984375; 187 | } 188 | } 189 | 190 | /** 191 | * Updates handler, this is where the game state should be updated. 192 | * 193 | * @return void 194 | */ 195 | public function update(EntitiesInterface $entities) : void 196 | { 197 | $playerEntity = $entities->firstWith(PlayerComponent::class); 198 | $playerTransform = $entities->get($playerEntity, Transform::class); 199 | $playerComponent = $entities->get($playerEntity, PlayerComponent::class); 200 | $playerSprite = $entities->get($playerEntity, SpriteComponent::class); 201 | 202 | $traveledDistance = $playerTransform->position->x; 203 | 204 | // calculate the current pipe index 205 | $pipeCount = $this->getPipeCount(); 206 | 207 | // calculate the current pipe index this is equal to the score of the player 208 | $centerPipeIndex = (int) floor(max(0, ($traveledDistance - $this->pipeStartOffset + $this->pipeDistance) / $this->pipeDistance)); 209 | 210 | // update the games score based on it 211 | $globalState = $entities->getSingleton(GlobalStateComponent::class); 212 | $globalState->score = $centerPipeIndex; 213 | $globalState->highScore = max($globalState->highScore, $globalState->score); 214 | 215 | // now our starting pipe is always the center pipe - half the pipe count 216 | $pipeIndex = (int) max(0, $centerPipeIndex - (int) floor($pipeCount / 2)); 217 | 218 | // we also generate the collison aabbs here 219 | // this is super waste full but we have tons of cpu time to spare 220 | $aabbs = []; 221 | // we build the AABBB on the basis of the sprite and then use our 222 | // transform to transform it into world space 223 | $spriteAABB = new AABB2D( 224 | new Vec2(-1, -1), 225 | new Vec2(1, 1) 226 | ); 227 | 228 | // create a floor and ceiling aabb 229 | $floorAABB = $spriteAABB->copy(); 230 | $floorAABB->min->y = -450; 231 | $floorAABB->max->y = -50; 232 | $floorAABB->min->x = $this->pipeStartOffset + $this->pipeSize; 233 | // if could make the floor and ceiling collider follow the player 234 | // or make this value bigger, but i decided if you make it this far you deserve a bug 235 | $floorAABB->max->x = 10000; 236 | 237 | $ceilingAABB = $spriteAABB->copy(); 238 | $ceilingAABB->min->y = 50; 239 | $ceilingAABB->max->y = 500; 240 | $ceilingAABB->min->x = $this->pipeStartOffset; 241 | $ceilingAABB->max->x = 10000; 242 | 243 | // add the floor and ceiling aabb 244 | $aabbs[] = $floorAABB; 245 | $aabbs[] = $ceilingAABB; 246 | 247 | // make more pipes when we are running out of pipes 248 | while (($pipeIndex + count($this->pipeEntities)) > count($this->pipeHeights)) { 249 | $this->generateMorePipes(); 250 | } 251 | 252 | // translate the existing pipes to their intended positions 253 | $it = 0; 254 | foreach($this->pipeEntities as $pipeGroup) 255 | { 256 | $pipeTopTransform = $entities->get($pipeGroup['pipeTop'], Transform::class); 257 | $pipeBottomTransform = $entities->get($pipeGroup['pipeBottom'], Transform::class); 258 | $outletTopTransform = $entities->get($pipeGroup['outletTop'], Transform::class); 259 | $outletBottomTransform = $entities->get($pipeGroup['outletBottom'], Transform::class); 260 | 261 | $renderPipeIndex = $pipeIndex + $it; 262 | 263 | $height = $this->pipeHeights[$renderPipeIndex]; 264 | $gabSize = $this->pipeGabs[$renderPipeIndex]; 265 | $pipeX = $renderPipeIndex * $this->pipeDistance + $this->pipeStartOffset; 266 | 267 | // close the pipes after the player has passed 50 of them 268 | // the closing speed determines, well how fast the pipes close.. 269 | // everything below 40 is almost impossible to pass we start to increase 270 | // closing speed at 100 points 271 | $difficutly = 150 * $globalState->closingPipesDifficulty; 272 | $closingSpeedDist = $difficutly - Math::clamp($globalState->score - $difficutly, 0, 30); 273 | 274 | if ($globalState->alwaysClosingPipes || $globalState->score > 50) { 275 | $playerDistnace = min(max($playerTransform->position->x - $pipeX, 0), $closingSpeedDist) / $closingSpeedDist; 276 | $gabSize = $gabSize * (1 - $this->bounceEaseOut($playerDistnace)); 277 | } 278 | 279 | // first we want to transform the outlets 280 | // the bottom outlets top position should be at gab size distance from the height 281 | $bottomY = $height - $gabSize / 2; 282 | $bottomY -= $this->pipeSize; 283 | 284 | $topY = $height + $gabSize / 2; 285 | $topY += $this->pipeSize; 286 | 287 | $outletBottomTransform->position = new Vec3($pipeX, $bottomY, 0); 288 | $outletBottomTransform->scale = new Vec3($this->pipeSize); 289 | $outletBottomTransform->markDirty(); 290 | 291 | $outletTopTransform->position = new Vec3($pipeX, $topY, 0); 292 | $outletTopTransform->scale = new Vec3($this->pipeSize, -$this->pipeSize, 1); 293 | $outletTopTransform->markDirty(); 294 | 295 | $pipeBottomTransform->position = new Vec3($pipeX, $bottomY - $this->viewportHeight, 0); 296 | $pipeBottomTransform->scale = new Vec3($this->pipeSize, $this->viewportHeight, 1); 297 | $pipeBottomTransform->markDirty(); 298 | 299 | $pipeTopTransform->position = new Vec3($pipeX, $topY + $this->viewportHeight, 0); 300 | $pipeTopTransform->scale = new Vec3($this->pipeSize, -$this->viewportHeight, 1); 301 | $pipeTopTransform->markDirty(); 302 | 303 | 304 | // construct the aabb 305 | $outpletTopAABB = $spriteAABB->copy(); 306 | $outpletTopAABB->applyTransform($outletTopTransform); 307 | $pipeTopAABB = $spriteAABB->copy(); 308 | $pipeTopAABB->applyTransform($pipeTopTransform); 309 | $topAABB = AABB2D::union($outpletTopAABB, $pipeTopAABB); 310 | 311 | $outletBottomAABB = $spriteAABB->copy(); 312 | $outletBottomAABB->applyTransform($outletBottomTransform); 313 | $pipeBottomAABB = $spriteAABB->copy(); 314 | $pipeBottomAABB->applyTransform($pipeBottomTransform); 315 | $bottomAABB = AABB2D::union($outletBottomAABB, $pipeBottomAABB); 316 | 317 | // D3D::aabb2D( 318 | // new Vec2(), 319 | // $topAABB->min, 320 | // $topAABB->max, 321 | // D3D::$colorRed 322 | // ); 323 | 324 | // D3D::aabb2D( 325 | // new Vec2(), 326 | // $bottomAABB->min, 327 | // $bottomAABB->max, 328 | // D3D::$colorGreen 329 | // ); 330 | 331 | $aabbs[] = $topAABB; 332 | $aabbs[] = $bottomAABB; 333 | 334 | $it++; 335 | } 336 | 337 | if ($playerComponent->dying) { 338 | return; 339 | } 340 | 341 | // construct the player aabb 342 | // for the player we are going to be a bit forgiving 343 | // and scale the abb a bit down 344 | $playerAABB = $spriteAABB->copy(); 345 | $playerAABB->applyTransform($playerTransform); 346 | $playerAABB->min->x = $playerAABB->min->x + 2.5; 347 | $playerAABB->max->x = $playerAABB->max->x - 2.5; 348 | $playerAABB->min->y = $playerAABB->min->y + 2.8; 349 | $playerAABB->max->y = $playerAABB->max->y - 4.0; 350 | 351 | // special case for other player sprites, yes this would be much 352 | // cleaner to store on the sprite component or even better 353 | // have a collider / aabb component. But this would require me 354 | // to change how the game resets after dying and i am lazy. 355 | if ($playerSprite->spriteName === 'helpium.png') { 356 | $playerAABB->min->x = $playerAABB->min->x + 3.5; 357 | } 358 | 359 | // D3D::aabb2D( 360 | // new Vec2(), 361 | // $playerAABB->min, 362 | // $playerAABB->max, 363 | // D3D::$colorGreen 364 | // ); 365 | 366 | // check if the player is colliding with any of the pipes 367 | foreach($aabbs as $aabb) { 368 | if ($aabb->intersects($playerAABB)) { 369 | Logger::info('Player collided with pipe'); 370 | $playerComponent->velocity = -1; 371 | $playerComponent->dying = true; 372 | } 373 | } 374 | } 375 | 376 | /** 377 | * Handles rendering of the scene, here you can attach additional render passes, 378 | * modify the render pipeline or customize rendering related data. 379 | * 380 | * @param RenderContext $context 381 | */ 382 | public function render(EntitiesInterface $entities, RenderContext $context) : void 383 | { 384 | // update the viewport width 385 | $cameraData = $context->data->get(CameraData::class); 386 | $this->viewportWidth = $cameraData->viewport->getWidth() * 1.4; // we want some overlap 387 | 388 | // update the number of pipes 389 | $this->ensurePipeEntities($entities, $this->getPipeCount()); 390 | 391 | DebugTextOverlay::debugString('pipes rendered: ' . $this->getPipeCount() . ' / pipe count: ' . count($this->pipeHeights)); 392 | } 393 | } -------------------------------------------------------------------------------- /src/System/RenderingSystem2D.php: -------------------------------------------------------------------------------- 1 | backgroundRenderer = new BackgroundRenderer($this->gl, $this->shaders); 55 | $this->spriteRenderer = new SpriteRenderer($this->gl, $this->shaders); 56 | $this->fullscreenRenderer = new FullscreenTextureRenderer($this->gl); 57 | $this->textLabelRenderer = new TextLabelRenderer($this->gl, $this->shaders); 58 | } 59 | 60 | /** 61 | * Registers the system, this is where you should register all required components. 62 | * 63 | * @return void 64 | */ 65 | public function register(EntitiesInterface $entities) : void 66 | { 67 | $entities->registerComponent(Transform::class); 68 | $entities->registerComponent(TextLabel::class); 69 | 70 | $this->textLabelRenderer->loadFont('debug', DebugOverlayTextRenderer::loadDebugFontAtlas()); 71 | } 72 | 73 | /** 74 | * Unregisters the system, this is where you can handle any cleanup. 75 | * 76 | * @return void 77 | */ 78 | public function unregister(EntitiesInterface $entities) : void 79 | { 80 | } 81 | 82 | /** 83 | * Updates handler, this is where the game state should be updated. 84 | * 85 | * @return void 86 | */ 87 | public function update(EntitiesInterface $entities) : void 88 | { 89 | $this->textLabelRenderer->synchroniseWithEntites($entities); 90 | } 91 | 92 | /** 93 | * Handles rendering of the scene, here you can attach additional render passes, 94 | * modify the render pipeline or customize rendering related data. 95 | * 96 | * @param RenderContext $context 97 | */ 98 | public function render(EntitiesInterface $entities, RenderContext $context) : void 99 | { 100 | // retrieve the backbuffer and clear it 101 | $backbuffer = $context->data->get(BackbufferData::class)->target; 102 | $context->pipeline->addPass(new ClearPass($backbuffer)); 103 | 104 | // fetch the camera data 105 | $cameraData = $context->data->get(CameraData::class); 106 | 107 | // create an intermediate 108 | $sceneRenderTarget = $context->pipeline->createRenderTarget('scene', $cameraData->resolutionX, $cameraData->resolutionY); 109 | 110 | // depth 111 | $sceneDepth = $context->pipeline->createDepthAttachment($sceneRenderTarget); 112 | 113 | $sceneColorOptions = new TextureOptions; 114 | $sceneColorOptions->internalFormat = GL_RGB; 115 | $sceneColor = $context->pipeline->createColorAttachment($sceneRenderTarget, 'sceneColor', $sceneColorOptions); 116 | 117 | // add the background pass 118 | $this->backgroundRenderer->attachPass($context->pipeline, $sceneRenderTarget, $entities); 119 | 120 | // add the image example pass 121 | $this->spriteRenderer->attachPass($context->pipeline, $sceneRenderTarget, $entities); 122 | 123 | // add the text label pass 124 | $this->textLabelRenderer->attachPass($context->pipeline, $sceneRenderTarget); 125 | 126 | // add a pass that renders the scene render target to the backbuffer 127 | $this->fullscreenRenderer->attachPass($context->pipeline, $backbuffer, $sceneColor); 128 | } 129 | } --------------------------------------------------------------------------------