I am working on a browser strategy game as a hobby and came up with a decision to use a tile-based map for the world map.
I have several questions so far:
I am using double-cache (file and apc) for performance issues. Isn't it too much? Or should I consider using Redis or anything similar?
What would be the best way to add gradients/transition zones to the images I am generating? For example if we have sand surrounded by sea cells, I would like the borders to be smooth between the cells, if you know what I mean :)
Thanks for any help in advance. Please see below for the code of the MapTile class that I am working on (the rest of the code can be found here).
<?php
/**
* TODO:
* - add gradients for neighbours tiles
*/
// namespace declaration
namespace Application\Model\Map;
use \PDO;
use \RecursiveDirectoryIterator;
use Application\Core\Cache;
use Application\Model\BaseModel;
use Application\Model\iModel;
use Application\Utils\Registry;
// file access protection
if (!defined('APP_PATH')) exit(_('Access denied'));
class MapTile extends BaseModel implements iModel
{
protected $table = 'map_coords_net';
const CELL_SIZE = 90;
const ZOOM_DEFAULT = 20;
const CACHE_PREFIX_IMG = 'tile_image_';
const CACHE_PREFIX_OBJ = 'tile_object_';
const CACHE_DIRECTORY = 'map/tiles';
public $coordsX = 0;
public $coordsY = 0;
public $mapGroundType = '';
protected $zoom = 0;
protected $image = null;
// neighbours types array for gradient rendering
protected $neighbours = array(
'north' => null,
'south' => null,
'west' => null,
'east' => null,
);
/**
* Get current tile and neighbours (4) details from db
*
* @return void
*/
public function getDetails()
{
// fetch from db
$stmt = $this->db->prepare("
SELECT
self.coordsX,
self.coordsY,
mgt.type as mapGroundType
FROM map_coords_net self
INNER JOIN map_ground_type mgt
ON self.mapGroundType = mgt.mapGroundTypeId
WHERE 1 = 1
-- get self and 5 neighbours if those exist
AND coordsX IN (:coordsX - 1, :coordsX, :coordsX + 1)
AND coordsY IN (:coordsY - 1, :coordsY, :coordsY + 1)
");
$stmt->bindParam(':coordsX', $this->coordsX, PDO::PARAM_INT);
$stmt->bindParam(':coordsY', $this->coordsY, PDO::PARAM_INT);
$stmt->execute();
// fetch request results
$tiles = $stmt->fetchAll(
PDO::FETCH_CLASS,
"\Application\Model\Map\MapTile"
);
// set neighbours array
foreach ($tiles as $tile)
{
// typecast
$tile->coordsX = (int) $tile->coordsX;
$tile->coordsY = (int) $tile->coordsY;
if ($this->equals($tile))
{
// copy tile properties
$this->mapGroundType = $tile->mapGroundType;
}
else {
// fill neignours array
$this->placeNeighbour($tile);
}
}
}
/**
* Get mapGroundTypeId from the `map_ground_type` table
*
* @return int
*/
public static function getMapGroundTypeByName($groundTypeName)
{
$groundTypeName = trim($groundTypeName);
$stmt = Registry::get('db')->prepare("
SELECT mapGroundTypeId
FROM map_ground_type
WHERE type = :type
");
$stmt->bindParam(':type', $groundTypeName, PDO::PARAM_STR);
$stmt->execute();
return (int) $stmt->fetchColumn();
}
/**
* Place neighbour in the corresponding array element
*
* @param MapTile $tile
*
* @return void
*/
protected function placeNeighbour(MapTile $tile)
{
if ($this->coordsX == $tile->coordsX - 1)
{
$this->neighbours['north'] = $tile;
}
if ($this->coordsX == $tile->coordsX + 1)
{
$this->neighbours['south'] = $tile;
}
if ($this->coordsY == $tile->coordsY - 1)
{
$this->neighbours['west'] = $tile;
}
if ($this->coordsY == $tile->coordsY + 1)
{
$this->neighbours['east'] = $tile;
}
}
/**
* Compare the passed tile to this one
*
* @param MapTile $tile
* @param string $compareBy - comparison method
*
* @return bool
*/
protected function equals(MapTile $tile, $compareBy = 'coords')
{
switch ($compareBy)
{
default:
case 'coords':
// return comapre results
return (
$this->coordsX == $tile->coordsX
&& $this->coordsY == $tile->coordsY
);
break;
}
return false;
}
/**
* Set tile coords
*
* @param int $x
* @param int $y
* @param int $z
*
* @return void
*/
public function setCoords($x, $y, $z = 20)
{
$this->coordsX = (int) $x;
$this->coordsY = (int) $y;
$this->zoom = (int) $z;
$this->getDetails();
}
/**
* Set image header for the output
*
* @return void
*/
public static function setImageHeader()
{
$cacheLifeTime = Cache::DEFAULT_TTL;
$ts = gmdate("D, d M Y H:i:s", time() + $cacheLifeTime) . " GMT";
header("Expires: {$ts}");
header("Pragma: cache");
header("Cache-Control: max-age={$cacheLifeTime}");
header('Content-type: image/jpeg');
}
/**
* Create image object
*
* Double-cached in files and apc
*
* @return void
*/
public function render()
{
$cacheKey = self::CACHE_PREFIX_IMG.$this->coordsX.':'.$this->coordsY;
$cacheFilePath = CACHE_PATH.self::CACHE_DIRECTORY.DIRECTORY_SEPARATOR.$cacheKey.'.jpg';
$id = array(
'coordsX' => $this->coordsX,
'coordsY' => $this->coordsY,
);
self::setImageHeader();
// check if cached file exists
if (file_exists($cacheFilePath))
{
if (Cache::exists($cacheKey)) {
$imageContent = unserialize(Cache::get($cacheKey));
}
else {
// get from file
$imageContent = file_get_contents($cacheFilePath);
// save in apc
Cache::set($cacheKey, serialize($imageContent));
}
// show file contents
echo $imageContent;
return;
}
// or create an image
else {
// create image object
$this->image = ImageCreateTrueColor(self::CELL_SIZE, self::CELL_SIZE);
if ($this->exists($id))
{
switch ($this->mapGroundType)
{
default:
case 'sea':
$backgroundColor = array_map('hexdec', str_split('0099FF', 2));
$noizeColor = array_map('hexdec', str_split('004C80', 2));
break;
case 'stone':
case 'mountain':
$backgroundColor = array_map('hexdec', str_split('969BA0', 2));
$noizeColor = array_map('hexdec', str_split('5A5D60', 2));
break;
case 'sand':
$backgroundColor = array_map('hexdec', str_split('CC9900', 2));
$noizeColor = array_map('hexdec', str_split('996600', 2));
break;
case 'forest':
$backgroundColor = array_map('hexdec', str_split('336600', 2));
$noizeColor = array_map('hexdec', str_split('003300', 2));
break;
case 'grass':
$backgroundColor = array_map('hexdec', str_split('339933', 2));
$noizeColor = array_map('hexdec', str_split('006600', 2));
break;
}
// create colors from RGB
$backgroundColorRGB = ImageColorAllocate($this->image, $backgroundColor[0], $backgroundColor[1], $backgroundColor[2]);
$noizeColorRGB = ImageColorAllocate($this->image, $noizeColor[0], $noizeColor[1], $noizeColor[2]);
// add noize
for ($w = 0; $w <= self::CELL_SIZE; $w++)
{
for ($h = 0; $h <= self::CELL_SIZE; $h++)
{
if (mt_rand(1, 100) >= 30) {
ImageSetPixel($this->image, $w, $h, $backgroundColorRGB);
}
else {
ImageSetPixel($this->image, $w, $h, $noizeColorRGB);
}
}
}
// black text for couloured tiles
$textcolor = ImageColorAllocate($this->image, 0, 0, 0);
}
else {
// display black rectangle for non-existing tile
$backgroundColorRGB = imagecolorallocate($this->image, 0, 0, 0);
imagefilledrectangle($this->image, 0, 0,
self::CELL_SIZE, self::CELL_SIZE,
$backgroundColorRGB
);
// white text for couloured tiles
$textcolor = ImageColorAllocate($this->image, 255, 255, 255);
}
}
// set text
$coordsString = "{$this->coordsX}:{$this->coordsY}";
ImageString($this->image, 1, 5, 5, $coordsString, $textcolor);
// save to cache
ImageJpeg($this->image, $cacheFilePath);
// view
ImageJpeg($this->image);
ImageDestroy($this->image);
}
/**
* Clear tile cache, saved in files
*
* @return bool
*
*/
public static function flushFileCache()
{
$tiles = new RecursiveDirectoryIterator(CACHE_PATH.self::CACHE_DIRECTORY);
foreach ($tiles as $fileInfo) {
unlink($fileInfo->getRealPath());
}
return true;
}
}
?>
imagick
, but this project will most probably be started from the shared hosting environment, which is why I decided not to use the one. Could you please give me a hint on whatmod x-sendfile
is? \$\endgroup\$