Jusque là, c'était très simple; maintenant, c'est plus sérieux.
Avant d'aller plus loin, nous allons avoir besoin d'outils.
Quelque soit le jeu que l'on souhaite faire, il est quasiment systématiquement nécessaire d'avoir un éditeur de niveau, permettant au minimum des actions simples (sauver et charger, définir les tiles, placer les objets, départ et arrivée).
De plus, une question se pose, où sont les graphismes ?
Ahah, vous pensiez que j'allais faire le gentil graphiste qui aurait préparé un Tileset
prêt à être utilisé ?
Et non, vous allez devoir le faire vous même... mais vous serez guidé !
Dans notre cas, nous allons utiliser des graphismes existant.
Cependant, trouver un Tileset
propre, n'est pas simple.
C'est d’ailleurs plus simple de le préparer soit même (à condition de se créer les bon outils).
Pour rappel, un Tileset
(Tilesheet) est une image contenant tous les morceaux uniques formant un niveau une fois assemblés.
En général, un graphiste va réaliser des samples de niveau (des bouts uniquement) pour mettre en avant les tiles (c'est de plus une bonne méthode de réalisation).
Si il est gentil, il vous fournira un ou plusieurs Tileset
.
Dans le cas contraire, ce sera à vous d'extraire les tiles, afin de ne conserver qu'une fois le même tile.
Dans la théorie, l’opération est simple (mais en pratique... non, ce n'est pas trop dur !).
Il suffit par exemple de mettre dans une seule image tous les bouts de niveau (sur une bonne grille, en fonction de la dimension d'un tile), et d'écrire une fonction qui sauvegarde dans une image tous les tiles unique (une seule version d'un Tile
).
Ainsi, peut on reconstruire un Tileset
à partir d'un niveau.
C'est d’ailleurs ce que nous allons faire, en utilisant un levelrip
du 1er niveau de Super Mario Bros
.
(Je ne parlerais pas ici de comment ripper des graphismes de jeux, regardez dans la section Tutoriels pour en savoir plus.)
Cliquez sur l'image pour avoir le niveau (sauvegardez là dans le dossier resources/
dans la racine du projet):
Maintenant, ajouter un nouveau dossier dans resources/
: tiles/theme
Ajouter cette ligne dans la méthode load()
de la classe Scene
:
TileExtractor.start(UtilityMedia.get("smb_level1-1.png"), UtilityMedia.get("tiles.png"), 16, 16, 128, 64);
Compilez, exécutez, et allez voir dans le dossier resources/
Surprise surprise, grâce à l'utilitaire que je vous propose, vous avez récupéré les versions uniques de chaque tile !
(L'image que vous avez obtenu est différente, car je me suis permis de réorganiser les tiles plus proprement.)
version brute |
version modifiée |
Vous pouvez maintenant retirer la ligne que vous avez ajouté précédemment, et déplacer le fichier tilesheet généré dans un dossier tiles/
par exemple, accompagné des fichiers suivants:
Voici les etapes à suivre:
TileCollision
:
/**
* List of tile collisions.
*
* @author Pierre-Alexandre (contact@b3dgs.com)
*/
enum TileCollision
{
/** No collision. */
NONE;
}
Tile
, héritant de TilePlatform
.
/**
* Tile implementation.
*
* @author Pierre-Alexandre (contact@b3dgs.com)
*/
final class Tile
extends TilePlatform<TileCollision>
{
/**
* @see TilePlatform#TilePlatform(int, int, Integer, int, Enum)
*/
Tile(int width, int height, Integer pattern, int number, TileCollision collision)
{
super(width, height, pattern, number, collision);
}
/*
* TilePlatform
*/
@Override
public Double getCollisionX(Localizable localizable)
{
return null;
}
@Override
public Double getCollisionY(Localizable localizable)
{
return null;
}
}
Map
, héritant de MapTilePlatform
.
/**
* Map implementation.
*
* @author Pierre-Alexandre (contact@b3dgs.com)
*/
final class Map
extends MapTilePlatform<TileCollision, Tile>
{
/**
* Constructor.
*/
Map()
{
super(16, 16);
}
/*
* MapTilePlatform
*/
@Override
public Tile createTile(int width, int height, Integer pattern, int number, TileCollision collision)
{
return new Tile(width, height, pattern, number, collision);
}
@Override
public TileCollision getCollisionFrom(String collision)
{
try
{
return TileCollision.valueOf(collision);
}
catch (final NullPointerException exception)
{
return TileCollision.NONE;
}
}
}
Scene
, afin d'importer le level rip et de le sauvegarder en niveau:
/**
* Game loop designed to handle our little world.
*
* @author Pierre-Alexandre (contact@b3dgs.com)
*/
final class Scene
extends Sequence
{
/** Native resolution. */
private static final Resolution NATIVE = new Resolution(320, 240, 60);
/** Camera reference. */
private final CameraPlatform camera;
/** Map reference. */
private final Map map;
/**
* Constructor.
*
* @param loader The loader reference.
*/
Scene(Loader loader)
{
super(loader, Scene.NATIVE);
camera = new CameraPlatform(width, height);
map = new Map();
}
/*
* Sequence
*/
@Override
protected void load()
{
LevelRipConverter<Tile> rip = new LevelRipConverter<>();
rip.start(UtilityMedia.get("smb_level1-1.png"), map, UtilityMedia.get("tiles"));
camera.setLimits(map);
}
@Override
protected void update(double extrp)
{
if (keyboard.isPressedOnce(Key.ESCAPE))
{
end();
}
}
@Override
protected void render(Graphic g)
{
map.render(g, camera);
}
}
Après tant d'effort, vous devriez avoir le résultat suivant:
Vous remarquerez qu'en procédant de la sorte, le programme effectue une analyse du levelrip
à chaque lancement.
Il est évident que l'on pourrait faire mieux.
Il suffit pour cela de sauvegarder le niveau dans un fichier après l'avoir convertit.
Par la même occasion, nous n'aurons plus qu'à charger ce fichier pour avoir le niveau en mémoire !
Voyons comment procéder:
levelrip
en un fichier niveau et le sauvegarder, nous avons besoin du code suivant:
/**
* Import and save the level.
*/
private void importAndSave()
{
final LevelRipConverter<Tile> rip = new LevelRipConverter<>();
rip.start(UtilityMedia.get("smb_level1-1.png"), map, UtilityMedia.get("tiles"));
try (FileWriting file = File.createFileWriting(UtilityMedia.get("smb_level1-1.lvl"));)
{
map.save(file);
}
catch (final IOException exception)
{
Verbose.exception(Scene.class, "constructor", exception, "Error on saving map !");
}
}
@Override
protected void load()
{
importAndSave();
try (FileReading reading = File.createFileReading(UtilityMedia.get("smb_level1-1.lvl"));)
{
map.load(reading);
}
catch (final IOException exception)
{
Verbose.exception(Scene.class, "constructor", exception, "Error on loading map !");
}
camera.setLimits(map);
}
Nous avons maintenant accès au niveau depuis un fichier.
World
Afin de simplifier les étapes précédentes, et pour préparer la suite, nous allons utiliser une classe spécialement conçue à cet effet, qui servira également à gérer les prochains composants.
World
de type WorldGame
:
/**
* World implementation.
*
* @author Pierre-Alexandre (contact@b3dgs.com)
*/
final class World
extends WorldGame
{
/** Camera reference. */
private final CameraPlatform camera;
/** Map reference. */
private final Map map;
/**
* @see WorldGame#WorldGame(Sequence)
*/
World(Sequence sequence)
{
super(sequence);
camera = new CameraPlatform(width, height);
map = new Map();
}
/*
* WorldGame
*/
@Override
public void update(double extrp)
{
// Update
}
@Override
public void render(Graphic g)
{
map.render(g, camera);
}
@Override
protected void saving(FileWriting file) throws IOException
{
map.save(file);
}
@Override
protected void loading(FileReading file) throws IOException
{
map.load(file);
camera.setLimits(map);
camera.setIntervals(16, 0);
}
}
Scene
afin de gérer une instance de World
:
/**
* Game loop designed to handle our little world.
*
* @author Pierre-Alexandre (contact@b3dgs.com)
*/
final class Scene
extends Sequence
{
/** Native resolution. */
private static final Resolution NATIVE = new Resolution(320, 240, 60);
/** World reference. */
private final World world;
/**
* Constructor.
*
* @param loader The loader reference.
*/
Scene(Loader loader)
{
super(loader, Scene.NATIVE);
world = new World(this);
}
/*
* Sequence
*/
@Override
protected void load()
{
world.loadFromFile(UtilityMedia.get("smb_level1-1.lvl"));
}
@Override
protected void update(double extrp)
{
world.update(extrp);
if (keyboard.isPressedOnce(Key.ESCAPE))
{
end();
}
}
@Override
protected void render(Graphic g)
{
world.render(g);
}
}
Désormais, c'est la classe World
qui gère le niveau, et c'est la Scene
qui lui fait charger le niveau voulu.
Lire la suite: Jeu de Plateforme - Le Joueur