Dans le précédent article sur motion-game nous avons survolé plusieurs fonctionnalités, il est temps maintenant d’approfondir celles-ci en nous penchant sur la notion de sprites !
Pour cela nous allons récupérer le précédent projet auquel nous allons ajouter un décor ainsi qu’une animation de notre survivant.
Un sprite est une image à laquelle on pourra ajouter des propriétés physiques pour interagir avec le jeu. Vous pouvez utiliser les ressources présentes sur notre compte github pour réaliser les différentes étapes de l’article.
Pour poser notre décor nous allons respecter une hiérarchie sur un axe imaginaire z
perpendiculaire à x
et y
. Cette hiérarchie est en fait celle du parser qui donnera la priorité d’affichage au dernier appelé, ainsi :
def initialize
add_hero
add_ground
end
def add_hero
# Some code to define hero
end
def add_ground
# Some code to define ground
end
Affichera le sol au dessus de notre personnage, il faudra donc inverser l’ordre d’appel des méthodes pour avoir le comportement souhaité.
Nous souhaitons ajouter un carrelage sur notre sol ! Nous allons dans un premier temps faire un tableau des coordonnées où seront positionnées nos textures.
Pour cela nous devons connaître la taille de notre écran et de notre texture.
# return screen sizes (width and height)
MG::Director.shared.size.width
MG::Director.shared.size.height
# return sprite sizes
sprite = MG::Sprite.new("ma_texture.png")
sprite.size.width
sprite.size.height
En divisant la taille de l’écran par la taille d’une image sur un axe, nous obtenons le nombre d’occurences nécessaires. Chaque sprite devra prendre en compte l’emplacement précédent pour se positionner au bon endroit.
scenes/main_scene.rb
class MainScene < MG::Scene
def initialize(name)
self.gravity = [0, 0]
# Some code ...
add_ground
add_survivor(name.downcase)
add_zombie
# Some code ...
end
def add_ground
sprite = MG::Sprite.new("ground.png")
axes = screen_coordinates(sprite)
axes.each do |axis|
ground = MG::Sprite.new("ground.png")
ground.anchor_point = [0, 0]
ground.position = [axis[0], axis[1]]
add ground
end
end
def screen_coordinates(sprite)
x_sprites = (scene_size[:width] / sprite.size.width).to_i + 1
y_sprites = (scene_size[:height] / sprite.size.height).to_i + 1
coordinates = []
x_sprites.times do |x|
y_sprites.times do |y|
coordinates << [x * sprite.size.width, y * sprite.size.height]
end
end
coordinates
end
# @return [Object] screen sizes (width and height)
def scene_size
{
width: MG::Director.shared.size.width,
height: MG::Director.shared.size.height
}
end
end
la méthode screen_coordinates(sprite)
prend comme paramètre un sprite et retourne un tableau de coordonnées partant de l’origine de notre sprite.
Ici nous voyons en vert notre sprite qui se répète, le tableau retourné contient les coordonnées des points rouges.
Dans le précédent article nous avons vu que par défaut la position d’un objet se fait par son centre, souvenez vous du “Hello world”.
Dans notre cas on aurait besoin que screen_coordinates(sprite)
retourne les coordonnés des points rouges ci-dessous.
Pour régler ce problème, la méthode add_ground
modifie le point d’encrage de notre sprite grâce à la méthode anchor_point
que l’on définit à l’origine [0, 0]
. On pourra donc utiliser les coordonnées retournées.
Continuons avec l’ajout des murs qui délimiteront notre scène et devront empêcher nos personnages de sortir de l’écran.
scenes/main_scene.rb
def add_horizontal_walls
sprite = MG::Sprite.new("sprites/top-wall-x.png")
coordinates = coordinate_bounds(sprite, "width")
coordinates.each do |coordinate|
wall = MG::Sprite.new("sprites/top-wall-x.png")
wall.anchor_point = coordinate[1] == 0 ? [0, 0] : [0, 1]
wall.attach_physics_box
wall.dynamic = false
wall.position = [coordinate[0], coordinate[1]]
add wall
end
end
def coordinate_bounds(sprite, axis)
axis_sprites = (scene_size[axis.to_sym] / sprite.size.method(axis).call).to_i
reverse_size = axis == "width" ? scene_size[:height] : scene_size[:width]
coordinates = []
2.times do |bound|
bound_coordinate = bound == 0 ? bound : reverse_size
axis_sprites.times do |axis_sprite|
coordinates << if axis == "width"
[axis_sprite * sprite.size.width, bound_coordinate]
else
[bound_coordinate, axis_sprite * sprite.size.height]
end
end
end
coordinates
end
Ici nous ajoutons des murs horizontaux en haut et en bas de notre scène.
Ces murs sont des objets physiques wall.attach_physics_box
qu’on ne peut pas déplacer wall.dynamic = false
.
Pour pimenter le jeu, vous pouvez rajouter aux murs la propriété de contact des personnages wall.contact_mask = 1
, il deviendra alors un élément à ne pas toucher.
Pour obtenir ce résultat il aura suffit de faire la même chose sur la verticale.
Pour finaliser nos murs, nous ajoutons un effet de perspective ainsi que les angles aux quatre coins de notre scène.
scenes/main_scene.rb
def add_wall_effect
sprite = MG::Sprite.new("sprites/wall.png")
coordinates = coordinate_bounds(sprite, "width")
coordinates.each do |coordinate|
wall = MG::Sprite.new("sprites/wall.png")
anchor = coordinate[1] == 0 ? [0, 0] : [0, 1]
wall.anchor_point = anchor
wall.position = [coordinate[0], coordinate[1] - wall.size.height]
add wall
end
end
def add_corners
top_left = MG::Sprite.new("sprites/corner-tl.png")
top_right = MG::Sprite.new("sprites/corner-tr.png")
bottom_left = MG::Sprite.new("sprites/corner-bl.png")
bottom_right = MG::Sprite.new("sprites/corner-br.png")
[top_left, top_right, bottom_left, bottom_right].each do |corner|
corner.attach_physics_box
corner.dynamic = false
end
top_left.anchor_point, top_left.position = [0, 1], [0, scene_size[:height]]
bottom_left.anchor_point, bottom_left.position = [0, 0], [0, 0]
top_right.anchor_point, top_right.position = [1, 1], [scene_size[:width], scene_size[:height]]
bottom_right.anchor_point, bottom_right.position = [1, 0], [scene_size[:width], 0]
add top_left
add top_right
add bottom_left
add bottom_right
end
Nous réutilisons la même méthode coordinate_bounds(sprite, type)
pour positionner notre effet de perspective. Il nous suffit alors de décaller notre perspective de la hauteur du mur.
Notez qu’il est important d’ajouter le sol en premier, l’effet de perspective du mur, les hauts de murs et enfin les angles.
def initialize(name)
# Some code
add_ground
add_wall_effect
add_vertical_walls
add_horizontal_walls
add_corners
# Some code
end
Pour simplifier l’animation de notre survivant, nous allons limiter les déplacements de celui-ci sur les axes x
et y
.
On va donc chercher à définir si la direction indiquée par le joueur est plutôt horizontale ou verticale.
Comme le montre ce schéma, on donnera la priorité à l’axe x
en cas de distance égale.
scenes/main_scene.rb
def direction(touch)
distance_x = (touch.location.x - @survivor.position.x).abs
distance_y = (touch.location.y - @survivor.position.y).abs
if distance_y > distance_x
[@survivor.position.x, touch.location.y]
else
[touch.location.x, @survivor.position.y]
end
end
Avec la méthode direction(touch)
, on détermine si le déplacement sera sur x
ou sur y
en fonction de touch
qui est le point désigné par le joueur. La méthode nous retournera un tableau contenant les coordonnées de déplacement.
Pour animer notre héro nous aurons besoin d’images montrant notre survivant sous plusieurs angles, pour cela j’ai utilisé un site qui génère toutes les images nécessaires.
Pour faire au plus simple, on utilisera deux images pour chaque déplacement en plus de l’image de notre survivant à l’arrêt.
Pour appeler simplement nos images nous allons adopter la convention suivante :
On aura donc deux images [direction]-[numero image].png
pour chaque déplacement, il nous reste à l’implémenter.
def initialize(name)
@hero_path = "characters/#{ name.downcase }"
# Some code ...
end
def animate_survivor(direction)
img_y = image_direction(direction, "y")
img_x = image_direction(direction, "x")
frames = if direction[0] == @survivor.position.x
define_frames("#{ @hero_path }/#{ img_y }")
else
define_frames("#{ @hero_path }/#{ img_x }")
end
@survivor.animate(frames, 0.2, 2)
end
def image_direction(direction, axis)
if axis == "y"
(direction[1] - @survivor.position.y) < 0 ? "face" : "back"
elsif axis == "x"
(direction[0] - @survivor.position.x) < 0 ? "left" : "right"
else
raise "Axis error"
end
end
def define_frames(path)
[1, 2].map { |i| "#{ path }-#{ i }.png" }
end
La méthode animate_survivor(direction)
va d’abord éliminer les images opposées à l’endroit défini par le joueur en passant son paramètre de direction à la méthode image_direction(direction, axis)
.
On vérifiera ensuite les images à animer en excluant l’axe dont la coordonnée correspond à la position de notre survivant.
Cette récupération d’image se fera à l’aide de la méthode define_frames(path)
, qui retourne les chemins des images.
Finalement nous avons bien le comportement souhaité !
Pour continuer à améliorer notre jeu nous ajoutons simplement deux boutons permettant de relancer la partie ou d’en créer une autre sur l’écran de game over.
def initialize(score, name)
add game_over_label
add score(score)
add retry_button(name)
add new_button
end
# Some code ...
def new_button
button = MG::Button.new("New game")
button.font_size = 35
button.position = [scene_center[:x] - 120, scene_center[:y] - 140]
button.color = [1, 0.8, 0.6]
button.on_touch { MG::Director.shared.replace(SurvivorScene.new) }
end
def retry_button(name)
button = MG::Button.new("Retry")
button.font_size = 35
button.position = [scene_center[:x] + 120, scene_center[:y] - 140]
button.color = [1, 0.8, 0.6]
button.on_touch { MG::Director.shared.replace(MainScene.new("#{ name }")) }
end
Grâce aux sprites vous pouvez maintenant décorer vos scènes et animer vos personnages pour enrichir la qualité de vos jeux smartphones !
J’espère que cet article vous aura plu en attendant les prochains niveaux !
L’équipe Synbioz. Libres d’être ensemble.