Blog tech

Motiongame - Les sprites

Rédigé par Nicolas Le Chenic | 7 mars 2016

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 ?!

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.

Ajout du décor

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é.

Le sol

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.

Les murs

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

Animons notre survivant

Limiter les déplacements

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.

Animation de notre survivant

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é !

Rejouer

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

Conclusion

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.