├── .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 |
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 |
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 | }
--------------------------------------------------------------------------------