Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<div align="center">
<img src="logo.jpg" alt="Sendama 2d Game Engine" width="128" height="128" align="center">
<div align="center" style="margin: 32px">
<img src="logo.png" alt="Sendama 2d Game Engine" width="128" height="128" align="center">
</div>

# Sendama &mdash; A 2D Game Engine for Terminal Based Games
Expand Down
4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
"name": "sendamaphp/engine",
"description": "A simple game engine for making terminal/console games. Lovingly written in pure PHP.",
"require-dev": {
"pestphp/pest": "^2.34",
"phpstan/phpstan": "^1.10"
"phpstan/phpstan": "^1.10",
"pestphp/pest": "^4.3"
},
"license": "MIT",
"autoload": {
Expand Down
1,271 changes: 742 additions & 529 deletions composer.lock

Large diffs are not rendered by default.

Binary file removed logo.jpg
Binary file not shown.
Binary file added logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
72 changes: 72 additions & 0 deletions src/Core/Rendering/SplashScreen.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php

namespace Sendama\Engine\Core\Rendering;

use Sendama\Engine\Debug\Debug;
use Exception;
use Sendama\Engine\Core\Enumerations\SettingsKey;
use Sendama\Engine\IO\Console\Console;
use Sendama\Engine\IO\Console\Cursor;
use Sendama\Engine\Util\Path;

final class SplashScreen
{
public function __construct(
private readonly Cursor $consoleCursor,
private readonly array $settings
)
{
}

public function show(): void
{
try {
Debug::info("Showing splash screen");
Console::setSize(MAX_SCREEN_WIDTH, MAX_SCREEN_HEIGHT);

// Check if a splash texture can be loaded
if (!file_exists($this->getSettings('splash_texture'))) {
Debug::warn("Splash screen texture not found: {$this->settings[SettingsKey::SPLASH_TEXTURE->value]}");
$this->settings[SettingsKey::SPLASH_TEXTURE->value] = Path::join(Path::getVendorAssetsDirectory(), DEFAULT_SPLASH_TEXTURE_PATH);
}

Debug::info("Loading splash screen texture");
$splashScreen = file_get_contents($this->getSettings('splash_texture'));
$splashScreenRows = explode("\n", $splashScreen);
$splashScreenWidth = 75;
$splashScreenHeight = 25;
$splashByLine = 'SendamaEngine ™';
$splashScreenRows[] = sprintf("%s%s", str_repeat(' ', $splashScreenWidth - 12), "powered by");
$splashScreenRows[] = sprintf("%s%s", str_repeat(' ', $splashScreenWidth - strlen($splashByLine)), $splashByLine);

$leftMargin = (MAX_SCREEN_WIDTH / 2) - ($splashScreenWidth / 2);
$topMargin = (MAX_SCREEN_HEIGHT / 2) - ($splashScreenHeight / 2);

Debug::info("Rendering splash screen texture");
foreach ($splashScreenRows as $rowIndex => $row) {
$this->consoleCursor->moveTo((int)$leftMargin, (int)($topMargin + $rowIndex));
Console::output()->write($row);
}

$duration = (int)($this->getSettings('splash_screen_duration') * 1000000);
usleep($duration);

Console::setSize($this->getSettings('screen_width'), $this->getSettings('screen_height'));
Console::clear();

Debug::info("Splash screen hidden");
} catch (Exception $exception) {
$this->handleException($exception);
}
}

private function getSettings(string $key)
{
return $this->settings[$key] ?? null;
}

private function handleException(Exception $exception): void
{
Debug::error("An error occurred while displaying the splash screen: " . $exception->getMessage());
}
}
6 changes: 3 additions & 3 deletions src/Core/Scenes/AbstractScene.php
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ public final function start(): void
{
Debug::info("Scene started: " . $this->name);

$this->createWordsSpace();
$this->createWorldSpace();
$this->loadStaticEnvironment();

foreach ($this->rootGameObjects as $gameObject) {
Expand Down Expand Up @@ -388,9 +388,9 @@ public function __unserialize(array $data): void
}

/**
* Creates the words space.
* Creates the world space.
*/
private function createWordsSpace(): void
private function createWorldSpace(): void
{
Debug::info('Creating world space for ' . $this->name);
$width = $this->settings['screen_width'];
Expand Down
15 changes: 15 additions & 0 deletions src/Core/Scenes/SceneManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@
use Sendama\Engine\Events\Enumerations\SceneEventType;
use Sendama\Engine\Events\EventManager;
use Sendama\Engine\Events\SceneEvent;
use Sendama\Engine\Exceptions\IncorrectComponentTypeException;
use Sendama\Engine\Exceptions\Scenes\SceneNotFoundException;
use Sendama\Engine\Physics\Interfaces\ColliderInterface;
use Sendama\Engine\Physics\Physics;

/**
* Class SceneManager. Manages the scenes of the game.
Expand Down Expand Up @@ -44,6 +47,7 @@ final class SceneManager implements SingletonInterface, CanStart, CanResume, Can
* @var EventManager $eventManager The event manager.
*/
protected EventManager $eventManager;
protected Physics $physics;

/**
* Constructs a SceneManager
Expand All @@ -52,6 +56,7 @@ private final function __construct()
{
$this->eventManager = EventManager::getInstance();
$this->scenes = new ItemList(SceneInterface::class);
$this->physics = Physics::getInstance();
}

/**
Expand Down Expand Up @@ -290,6 +295,16 @@ public function getSettings(?string $key = null): mixed
*/
public function load(): void
{
$this->physics->init();
foreach ($this->activeSceneNode->getScene()->getRootGameObjects() as $gameObject) {
if ($collider = $gameObject->getComponent(ColliderInterface::class)) {
assert($collider instanceof ColliderInterface, new IncorrectComponentTypeException(
ColliderInterface::class,
get_class($collider)
));
$this->physics->addCollider($collider);;
}
}
$this->activeSceneNode->getScene()->load();
$this->eventManager->dispatchEvent(new SceneEvent(SceneEventType::LOAD_END));
}
Expand Down
17 changes: 9 additions & 8 deletions src/Debug/Debug.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@

use RuntimeException;
use Sendama\Engine\Debug\Enumerations\LogLevel;
use Sendama\Engine\Exceptions\DebuggingException;
use Sendama\Engine\Util\Path;

/**
* Class Debug. A class for logging debug messages.
*
* @package Sendama\Engine\Debug
*/
class Debug
final class Debug
{
/**
* @var string|null $logDirectory The directory to write the log files to.
Expand Down Expand Up @@ -86,18 +87,18 @@ public static function log(

if (!file_exists($filename)) {
if (!is_writeable(self::getLogDirectory())) {
throw new RuntimeException("The directory, " . self::getLogDirectory() . ", is not writable.");
throw new DebuggingException("The directory, " . self::getLogDirectory() . ", is not writable.");
}

if (false === $file = fopen($filename, 'w')) {
throw new RuntimeException("Failed to create the debug log file.");
throw new DebuggingException("Failed to create the debug log file.");
}
fclose($file);
}

$message = sprintf("[%s] %s - %s", date('Y-m-d H:i:s'), $prefix, $message) . PHP_EOL;
if (false === error_log($message, 3, $filename)) {
throw new RuntimeException("Failed to write to the debug log.");
throw new DebuggingException("Failed to write to the debug log.");
}
}

Expand All @@ -118,19 +119,19 @@ public static function error(string $message, string $prefix = '[ERROR]'): void

if (!file_exists($filename)) {
if (!is_writeable(self::getLogDirectory())) {
throw new RuntimeException("The directory, " . self::getLogDirectory() . ", is not writable.");
throw new DebuggingException("The directory, " . self::getLogDirectory() . ", is not writable.");
}

if (false === $file = fopen($filename, 'w')) {
throw new RuntimeException("Failed to create the error log file.");
throw new DebuggingException("Failed to create the error log file.");
}

fclose($file);
}

$message = sprintf("[%s] %s - %s", date('Y-m-d H:i:s'), $prefix, $message) . PHP_EOL;
if (false === error_log($message, 3, $filename)) {
throw new RuntimeException("Failed to write to the debug log.");
throw new DebuggingException("Failed to write to the error log.");
}
}

Expand All @@ -155,4 +156,4 @@ public static function info(string $message, ?string $prefix = null): void
{
self::log($message, $prefix ?? '[INFO]', LogLevel::INFO);
}
}
}
13 changes: 13 additions & 0 deletions src/Exceptions/DebuggingException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace Sendama\Engine\Exceptions;

use RuntimeException;

final class DebuggingException extends RuntimeException
{
public function __construct(string $message, int $code = 0, ?\Throwable $previous = null)
{
parent::__construct("DebuggingException: " . $message, $code, $previous);
}
}
7 changes: 7 additions & 0 deletions src/Exceptions/IOException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

namespace Sendama\Engine\Exceptions;

class IOException extends SendamaException
{
}
33 changes: 19 additions & 14 deletions src/Game.php
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,18 @@ public function __construct(private readonly string $name, private readonly int
}
}

/**
* Destruct the game engine.
*/
public function __destruct()
{
Console::restoreSettings();

if ($lastError = error_get_last()) {
$this->handleError($lastError['type'], $lastError['message'], $lastError['file'], $lastError['line']);
}
}

/**
* @return void
*/
Expand Down Expand Up @@ -204,7 +216,6 @@ protected function configureErrorAndExceptionHandlers(): void
private function handleException(Exception|Throwable|Error $exception): never
{
Debug::error($exception);
// Console::reset();
$this->stop();

if ($this->getSettings('debug')) {
Expand All @@ -221,6 +232,8 @@ private function handleException(Exception|Throwable|Error $exception): never
*/
public function stop(): void
{
Console::reset();

Debug::info("Stopping game");

// Disable non-blocking input mode
Expand Down Expand Up @@ -495,19 +508,6 @@ public static function quit(): void
}
}

/**
* Destruct the game engine.
*/
public function __destruct()
{
Console::restoreSettings();
Console::reset();

if ($lastError = error_get_last()) {
$this->handleError($lastError['type'], $lastError['message'], $lastError['file'], $lastError['line']);
}
}

/**
* Run the game.
*
Expand All @@ -525,6 +525,11 @@ public function run(): void
while ($this->isRunning) {
$this->handleInput();
$this->update();

if (!$this->isRunning) {
break;
}

$this->render();

usleep($sleepTime);
Expand Down
1 change: 1 addition & 0 deletions src/IO/Console/Console.php
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ public static function saveSettings(): void
public static function restoreSettings(): void
{
shell_exec('stty ' . self::$previousTerminalSettings);
Console::cursor()->enableBlinking();
}

/**
Expand Down
6 changes: 3 additions & 3 deletions src/IO/Input.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,9 @@ public static function areAllKeysPressed(array $keyCodes): bool
* @param array<KeyCode> $keyCodes
* @return bool Returns true if any key is pressed, false otherwise.
*/
public static function isAnyKeyPressed(array $keyCodes): bool
public static function isAnyKeyPressed(array $keyCodes, bool $ignoreCase = true): bool
{
return InputManager::isAnyKeyPressed($keyCodes);
return InputManager::isAnyKeyPressed($keyCodes, $ignoreCase);
}

/**
Expand Down Expand Up @@ -99,4 +99,4 @@ public static function isButtonDown(string $buttonName): bool
{
return InputManager::isButtonDown($buttonName);
}
}
}
Loading
Loading