Introduction au développement de jeu smartphone avec Motiongame

Publié le 29 octobre 2015 par Nicolas Le Chenic | mobile

Cet article est publié sous licence CC BY-NC-SA

Rubymotion a lancé en septembre dernier sa plateforme de création de jeux 2D pour smartphone (iOS et Android) nommée Motiongame.

Jusqu’à présent la création de jeux se faisait à l’aide de la gem Joybox. Aujourd’hui Rubymotion inclut une version bêta de Motiongame qui sera maintenue à jour, facile à mettre en place et cross-plateform !

Dans cet article nous allons dégrossir Motiongame pour, je l’espère, vous donner envie de développer des jeux smartphone !

Hello world

Avant tout, il vous faudra installer Motiongame, pour cela rien de compliqué.

gem install motion-game

Nous pouvons maintenant créer notre premier jeu qui sera un simple hello world.

motion create --template=motion-game hello
cd hello

Votre projet est créé avec peu de fichiers par défaut ce qui le rend facile à aborder.

Erreur d'attribut

Pour cette introduction nous allons nous intérésser au dossier app dans lequel on va développer un mini jeu.

app/application.rb

class Application < MG::Application
  def start
    MG::Director.shared.run(MainScene.new)
  end
end

app/main_scene.rb

class MainScene < MG::Scene
  def initialize
    label = MG::Text.new("Hello World", "Arial", 96)
    label.anchor_point = [0, 0]
    add label
  end
end

Par défaut tous les fichiers dans app sont chargés par Motiongame. Le point d’entrée de l’application se nomme application.rb et la scène où se déroule le jeu main_scene.rb.

Le fichier application lance notre scène qui est un simple “Hello World” auquel on précise le style de police de caractère avant d’appliquer un point d’ancrage aux coordonnées d’origine de notre application.

Il est temps d’émuler notre jeu sur iOS et Android !

rake ios:simulator
rake android:emulator

Une fois lancé vous devriez voir “Hello World” apparaître.

Erreur d'attribut

Comme vous le voyez, il est très facile de créer un jeu iOS et Android avec un seul et même code !

Notre scène se contente pour le moment d’afficher un label prédéfini auquel vous pouvez appliquer plusieurs méthodes.

app/main_scene.rb

  def initialize
    label = MG::Text.new("Hello World", "Arial", 96)
    label.position = [400, 400]
    label.color = [0, 0.6, 0.8]
    label.rotation = -10.0

    add label
  end

La position va nous permettre de déplacer notre label par rapport à son centre. On modifie aussi la couleur du texte et on applique une légère rotation sur notre élément.

Erreur d'attribut

Synbioz vs Zombie

Continuons avec un mini jeu qui est un prétexte à la découverte de fonctionnalités. Il existe déjà un exemple de Flappy Bird proposé par l’équipe de RubyMotion que je vous invite à télécharger. Notre mini jeu bien que moins élaboré nous permettra de découvrir d’autres méthodes qui pourront compléter votre apprentissage.

Le jeu

Le but du jeu “Synbioz vs Zombie” est simple, vous incarnez un survivant de l’équipe qui doit fuir le zombie le plus longtemps possible.

Avant de commencer il vous faudra récupérer les ressources images et audio sur github.

Premiers pas

Pour réaliser notre jeu nous aurons besoin des trois scènes suivantes :

  • Choix du survivant (survivor_scene.rb)
  • La scène principale (main_scene.rb)
  • L’écran de game over (game_over_scene.rb)

C’est parti pour la création de notre jeu.

motion create --template=motion-game synbiozvszombie
cd synbiozvszombie

Dans le dossier app, on ajoute un sous-dossier scenes qui regroupera nos trois scènes sans oublier de supprimer main_scene.rb à la racine de app pour éviter les conflits.

Choix du survivant

Notre première scène va nous permettre de sélectionner le survivant de l’équipe synbioz que l’on souhaite incarner.

app/application.rb

class Application < MG::Application
  def start
    MG::Director.shared.run(SurvivorScene.new)
  end
end

Notre jeu se lance maintenant sur la scène souhaitée.

app/scenes/survivor_scene.rb

class SurvivorScene < MG::Scene
  def initialize

    add_label
  end

  def add_label
    label = MG::Text.new("Choose your survivor", "Arial", 80)
    label.color = [0.7, 0.7, 0.7]
    label.position = [MG::Director.shared.size.width / 2, MG::Director.shared.size.height - 100]

    add label
  end
end

Dans un premier temps nous ajoutons un simple texte au-dessous duquel nous pourrons choisir notre survivant.

MG::Director.shared.size.width nous permet de récupérer la largeur de l’écran. Une fois divisée par deux, notre texte sera centré sur x. On laisse ensuite une marge de 100px en haut de l’écran.

Il est maintenant temps d’ajouter la liste de nos survivants :

app/scenes/survivor_scene.rb

def add_survivors
  team_synbioz = ["Martin", "Nico", "Victor", "Jon", "Numa", "Clement", "Theo", "Cedric"]

  team_synbioz.each_with_index do |name, index|

    button = MG::Button.new("#{name}")
    button.font_size = 35
    button.position = [MG::Director.shared.size.width / 2, (MG::Director.shared.size.height - 200) - (index * 50)]
    button.on_touch { MG::Director.shared.replace(MainScene.new(name)) }

    add button
  end
end

Pour chacun des membres de Synbioz, on ajoute un bouton. Une fois cliqué, nous remplaçons la scène courante par la scène principale avec MG::Director.shared.replace() en spécifiant le nom du survivant en paramètre.

Vous devriez voir ceci à l’écran :

Erreur d'attribut

Scène principale, les choses sérieuses commencent

Notre scène principale comportera deux éléments, un survivant et un zombie. Le zombie se déplacera aléatoirement sur l’écran et vous devrez indiquer au survivant de se déplacer pour l’éviter.

Les ressources

Pour cela nous allons avoir besoin de ressources. Par défaut, Motiongame récupère ses ressources dans un dossier resources à la racine de notre application. Il suffit donc de créer ce dossier et d’y ajouter vos ressources.

app/scenes/main_scene.rb

  def initialize(name)
    add_zombie
  end

  def add_zombie
    @zombie = MG::Sprite.new("zombie.png")
    @zombie.position = [400, MG::Director.shared.size.height / 2]

    add @zombie
  end

Ici MG::Sprite.new("image") récupère notre image sur laquelle on appliquera les méthodes souhaitées. Il est tout aussi simple de lancer une musique extraite de ce dossier avec MG::Audio.play("song").

app/scenes/main_scene.rb

  def initialize(name)
    MG::Audio.play("night.wav", true, 0.5)
    @name = name.downcase

    add_zombie
  end

Ce son d’ambiance est joué au lancement de notre scène, true précisant qu’on souhaite qu’elle tourne en boucle. Enfin, 0.5 indique le volume sonore.

Nous allons maintenant importer l’image du survivant sélectionné qui est stockée dans le dossier resources/survivors.

Nous récupérons le nom dans la variable d’instance @name qui nous sert à afficher dynamiquement l’image de notre survivant.

app/scenes/main_scene.rb

  def initialize(name)
    MG::Audio.play("night.wav", true, 0.5)
    @name = name.downcase

    add_zombie
    add_survivor
  end

  def add_survivor
    @survivor = MG::Sprite.new("survivors/#{@name}.jpg")
    @survivor.position = [100, MG::Director.shared.size.height / 2]

    add @survivor
  end

Une fois votre choix effectué vous arrivez sur la scène principale.

Erreur d'attribut

Objets physiques

Nous souhaitons que lorsque nos personnages se touchent, la scène de game over se déclenche. Pour cela nous allons dans un premier temps déplacer notre survivant vers notre zombie pour observer le comportement par défaut.

app/scenes/main_scene.rb

  def initialize(name)
    MG::Audio.play("night.wav", true, 0.5)
    @name = name.downcase

    add_zombie
    add_survivor
  end

  def add_survivor
    @survivor = MG::Sprite.new("survivors/#{@name}.jpg")
    @survivor.position = [100, MG::Director.shared.size.height / 2]
    @survivor.move_to([450, @survivor.position.y], 1)

    add @survivor
  end

Erreur d'attribut

Pour le moment notre survivant traverse notre zombie, les deux étant de simples images. Il nous faut rendre ces images physiques pour qu’un contact soit possible.

app/scenes/main_scene.rb

  def add_zombie
    @zombie = MG::Sprite.new("zombie.png")
    @zombie.attach_physics_box
    @zombie.position = [400, MG::Director.shared.size.height / 2]

    add @zombie
  end

  def add_survivor
    y_position = MG::Director.shared.size.height / 2

    @survivor = MG::Sprite.new("survivors/#{@name}.jpg")
    @survivor.attach_physics_box
    @survivor.position = [100, y_position]
    @survivor.move_to([450, y_position], 1)

    add @survivor
  end

Cliquez sur l’image ci-dessous pour voir l’animation.

Erreur d'attribut

On a maintenant une collision entre notre survivant et le zombie ! On remarque aussi qu’ils sont maintenant soumis aux lois de la gravité ce qui est très intéressant dans un Mario 2D par exemple.

Notre mini jeu étant plus proche d’un Pacman, on va donc retirer cette gravité.

app/scenes/main_scene.rb

  def initialize(name)
    self.gravity = [0, 0]

    MG::Audio.play("night.wav", true, 0.5)
    @name = name.downcase

    add_zombie
    add_survivor
  end

Maintenant que nos personnages se comportent comme on le souhaite, il ne reste qu’à déclencher notre scène de game over lors du contact, pour cela on utilise le contact_mask.

app/scenes/main_scene.rb

  def initialize(name)
    self.gravity = [0, 0]

    MG::Audio.play("night.wav", true, 0.5)
    @name = name.downcase

    add_zombie
    add_survivor

    on_contact_begin do
      MG::Director.shared.replace(GameOverScene.new)
    end
  end

  def add_zombie
    @zombie = MG::Sprite.new("zombie.png")
    @zombie.attach_physics_box
    @zombie.position = [400, MG::Director.shared.size.height / 2]
    @zombie.contact_mask = 1

    add @zombie
  end

  def add_survivor
    y_position = MG::Director.shared.size.height / 2

    @survivor = MG::Sprite.new("survivors/#{@name}.jpg")
    @survivor.attach_physics_box
    @survivor.position = [100, y_position]
    @survivor.contact_mask = 1
    @survivor.move_to([450, y_position], 1)

    add @survivor
  end

app/scenes/game_over_scene.rb

class GameOverScene < MG::Scene
  def initialize
    add_label
  end

  def add_label
    label = MG::Text.new("Game over...", "Arial", 96)
    label.position = [MG::Director.shared.size.width / 2, MG::Director.shared.size.height / 2]
    add label
  end
end

Désormais, lorsque notre survivant touche le zombie, le bloc on_contact_begin est appelé et remplace notre scène par l’écran de game over !

Les déplacements

Nous souhaitons maintenant déplacer nos personnages. Le survivant devra se déplacer vers notre doigt dès qu’on touche l’écran. Le zombie se déplacera aléatoirement en respectant les limites de l’écran.

Déplacements par évènement

Nous allons d’abord déplacer notre survivant, pour cela nous avons besoin de récupérer les coordonnées de notre évènement que l’on utilisera dans @survivant.move_to([x, y], 1).

app/scenes/main_scene.rb

def initialize(name)
  self.gravity = [0, 0]

  @name = name.downcase

  add_survivor
  add_zombie

  on_touch_begin do |touch|
    @survivor.move_to([touch.location.x, touch.location.y], 1)
  end

  on_contact_begin do
    MG::Director.shared.replace(GameOverScene.new(@score))
  end
end

Le bloc on_touch_begin nous permet de récupérer les coordonnées de contact de notre doigt. Il ne reste plus qu’à retirer le déplacement précédent.

Déplacements aléatoires

Avec la méthode update, Motiongame inclut un système de boucle facile à mettre en place. Il suffit de définir une méthode update(delta) que l’on switch facilement grâce aux méthodes start_update et stop_update.

app/scenes/main_scene.rb

  def initialize(name)
    self.gravity = [0, 0]

    @name = name.downcase
    @zombie_update_position = 0

    # CODE ...

    start_update
  end

  def update(delta)

    @zombie_update_position += delta

    if @zombie_update_position >= 2.0
      @zombie.move_to([random_position[:x], random_position[:y]], 1)
      @zombie_update_position = 0
    end
  end

  private
  def random_position
    {
      x: Random.new.rand(0..MG::Director.shared.size.width),
      y: Random.new.rand(0..MG::Director.shared.size.height)
    }
  end

Dans un premier temps on initialise @zombie_update_position à zéro, puis on lance notre boucle avec le start_update. La méthode update a comme paramètre delta qui retourne le temps qu’a mis la boucle à s’exécuter. Ainsi, on peut récupérer le temps courant en incrémentant avec delta.

On se sert ici de ce principe pour déplacer notre zombie toutes les deux secondes. On précise ensuite des coordonnées aléatoires qui respectent les dimensions de l’écran pour gérer le déplacement du zombie.

Un peu de piment

Afin de pimenter un peu notre mini jeu, on va modifier l’échelle de notre zombie toutes les deux secondes, puis on affichera le temps de survie sur l’écran de game over.

app/scenes/main_scene.rb

def initialize(name)
  self.gravity = [0, 0]

  @name = name.downcase
  @time = 0
  @zombie_update_position = 0

  add_survivor
  add_zombie

  on_touch_begin do |touch|
    @survivor.move_to([touch.location.x, touch.location.y], 2)
  end

  on_contact_begin do
    MG::Director.shared.replace(GameOverScene.new(@time))
  end

  start_update
end

def add_zombie
  # CODE ...
end

def add_survivor
  # CODE ...
end

def update(delta)

  @time += delta
  @zombie_update_position += delta

  if @zombie_update_position >= 2.0
    @zombie.move_to([random_position[:x], random_position[:y]], 1)
    @zombie.scale += 0.1
    @zombie_update_position = 0
  end
end

private
def random_position
  {
    x: Random.new.rand(0..MG::Director.shared.size.width),
    y: Random.new.rand(0..MG::Director.shared.size.height)
  }
end

On utilise la variable @time que l’on transmet à la scène de game over.

app/scenes/game_over_scene.rb

class GameOverScene < MG::Scene
  def initialize(score)
    add_label
    add_score(score)
  end

  def add_label
    label = MG::Text.new("Game over...", "Arial", 96)
    label.position = [MG::Director.shared.size.width / 2, MG::Director.shared.size.height / 2]
    add label
  end

  def add_score(score)
    time = score.round(2)
    score = MG::Text.new("You survived #{time}s", "Arial", 40)
    score.position = [MG::Director.shared.size.width / 2, (MG::Director.shared.size.height / 2) - 80]

    add score
  end
end

Erreur d'attribut

Erreur d'attribut

Conclusion

Dans cette introduction nous avons survolé beaucoup de fonctionnalités de Motiongame. Si le sujet vous intéresse, je ne saurais trop vous conseiller de faire un tour sur la documentation en attendant un prochain article où on entrera dans le détail !


L’équipe Synbioz. Libres d’être ensemble.