"Les cours de neeko.fr"

Retour en haut

Le choc des titans, version complète

Le choc des titans, version complète

Objectif

L'objectif est de réaliser l'application complete du choc des titans.

La première étape dans la réalisation d'un projet est de décider à l'avance son périmètre. Plusieurs méthodes et outils existent, comme par exemple les mockups :

L'application finale :

Le périmètre défini et validé avec tous les participants du projet, il est judicieux de découper en lots.

Version 1.0

La première version du jeu ne permettra pas au joueur de choisir son action, et ne comportera que 2 images par personnage (normal ou perdu).

Après avoir crée le projet, création des 2 activities : HomeActivity et GameActivity.

HomeActivity

Le but est d'avoir une image et un bouton vers la 2eme activity.

On commence par ajouter le fichier image logo.png dans les dossiers de ressource.

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center_vertical|center_horizontal" android:orientation="vertical" > <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="Android102/@drawable/logo" android:padding="@dimen/basePadding" android:contentDescription="@string/app_name" /> <Button android:id="@+id/startButton" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/start_game" /> </LinearLayout>

package fr.neeko.lechocdestitans; import android.os.Bundle; import android.app.Activity; import android.content.Intent; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; public class HomeActivity extends Activity implements OnClickListener { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_home); Button start = (Button) findViewById(R.id.startButton); start.setOnClickListener(this); } @Override public void onClick(View v) { Intent i = new Intent(this, GameActivity.class); startActivity(i); } }

GameActivity et le jeu

Avant d'attaquer l'activity principale, il faut créer les classes que nous allons manipuler.

La classe représentant un personnage : Character

package fr.neeko.lechocdestitans; public class Character { protected String name; protected int currentLife; protected String currentState; //normal, loose protected int normalImageResource; protected int looseImageResource; protected OnCharacterChangeListener listener; public Character(String name, int life){ this.name = name; this.currentLife = life; currentState="normal"; } public void setOnCharacterChangeListener(OnCharacterChangeListener l){ this.listener = l; } public void evaluate(){ if (! this.isAlive()){ this.currentState = "loose"; this.triggerListener(); return; } this.currentState = "normal"; this.triggerListener(); } public void receiveDamages(int damages){ this.currentLife = this.currentLife - damages; this.triggerListener(); } public int getDamages() { int maxDamages = 5; int damages = (int) (Math.round(Math.random() * maxDamages) + 1); return damages; } public String getName(){ return this.name; } public int getLife(){ return this.currentLife; } public boolean isAlive(){ if (this.currentLife > 0){ return true; } else { return false; } } public void triggerListener(){ if (this.listener != null){ this.listener.characterChange(this); } } public int getImageResource(){ if (this.currentState.equals("loose")){ return this.looseImageResource; } else { return this.normalImageResource; } } }

package fr.neeko.lechocdestitans; public interface OnCharacterChangeListener { public void characterChange(Character c); }

Afficher un personnage : CharacterDisplay

Le personnage doit être représenté à chaque changement (perte de point de vie, changement d'état ...).

Ici, on se sert simplement des éléments classiques d'Android pour représenter notre personnage.

package fr.neeko.lechocdestitans; import android.widget.ImageView; import android.widget.TextView; public class CharacterDisplay implements OnCharacterChangeListener { public ImageView imageView; public TextView nameLabel; public TextView lifeLabel; @Override public void characterChange(Character c) { this.imageView.setImageResource(c.getImageResource()); this.nameLabel.setText(c.getName()); this.lifeLabel.setText("" + c.getLife()); } }

Le combat

On encapsule les règles de notre combat dans cette classe.

Il aurait été possible d'avoir des variantes (nombre de tour limité, plus de participants, ...).

Notez la seule méthode public. Notez aussi l'appel de méthode récursif.

package fr.neeko.lechocdestitans; public class Combat { public CombatListener listener; public Character character1; public Character character2; public int currentTurn; public void startCombat(){ this.currentTurn = 0; this.beginTurn(); } protected void beginTurn(){ this.currentTurn++; this.listener.combatTurnStart(this); this.character1.receiveDamages(this.character2.getDamages()); this.character2.receiveDamages(this.character1.getDamages()); this.character1.evaluate(); this.character2.evaluate(); if (! this.character1.isAlive() || ! this.character2.isAlive() ){ this.finishCombat(); return; } this.beginTurn(); } protected void finishCombat(){ if (this.character1.isAlive()){ this.listener.combatEnd(this, this.character1); } else if (this.character2.isAlive()){ this.listener.combatEnd(this, this.character2); } else { this.listener.combatEndExAequo(this); } } }

L'interface CombatListener permet de notifier "l'extérieur" de l'évolution du combat.

package fr.neeko.lechocdestitans; public interface CombatListener { public void combatTurnStart(Combat c); public void combatEnd(Combat c, Character winner); public void combatEndExAequo(Combat c); }

L'activity et son layout

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:clipChildren="false" android:clipToPadding="false" android:id="@+id/gameLayout" > <LinearLayout android:id="@+id/p1Layout" android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="center_horizontal" android:orientation="vertical" android:padding="@dimen/basePadding" android:clipChildren="false" android:clipToPadding="false" android:layout_alignParentLeft="true"> <TextView android:id="@+id/p1Name" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textAppearance="?android:attr/textAppearanceLarge" /> <ImageView android:id="@+id/p1Portrait" android:clipChildren="false" android:clipToPadding="false" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <TextView android:id="@+id/p1Life" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </LinearLayout> <LinearLayout android:id="@+id/p2Layout" android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="center_horizontal" android:orientation="vertical" android:layout_alignParentRight="true"> <TextView android:id="@+id/p2Name" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textAppearance="?android:attr/textAppearanceLarge" /> <ImageView android:id="@+id/p2Portrait" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <TextView android:id="@+id/p2Life" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </LinearLayout> <TextView android:id="@+id/messageZone" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_alignParentLeft="true" android:padding="@dimen/basePadding" android:text="@string/combat_start" /> <Button android:id="@+id/buttonNext" android:text="@string/start_combat" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_alignParentRight="true" /> </RelativeLayout>

L'assemblage dans l'activity

Le principe :

package fr.neeko.lechocdestitans; import android.os.Bundle; import android.app.Activity; import android.util.Log; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; import android.widget.ImageView; import android.widget.TextView; public class GameActivity extends Activity implements CombatListener, OnClickListener { protected Combat combat; protected TextView messageView; protected Button nextButton; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_game); //construction du premier personnage Character player = new Character("Ryu", 20); player.normalImageResource = R.drawable.player_normal; player.looseImageResource = R.drawable.player_loose; //construction du second personnage Character boss = new Character("Zangief", 20); boss.normalImageResource = R.drawable.evil_normal; boss.looseImageResource = R.drawable.evil_loose; //construction de la classe qui afficher le premier personnage CharacterDisplay displayPlayer = new CharacterDisplay(); displayPlayer.imageView = (ImageView) findViewById(R.id.p1Portrait); displayPlayer.nameLabel = (TextView) findViewById(R.id.p1Name); displayPlayer.lifeLabel = (TextView) findViewById(R.id.p1Life); //construction de la classe qui afficher le 2e personnage CharacterDisplay displayBoss = new CharacterDisplay(); displayBoss.imageView = (ImageView) findViewById(R.id.p2Portrait); displayBoss.nameLabel = (TextView) findViewById(R.id.p2Name); displayBoss.lifeLabel = (TextView) findViewById(R.id.p2Life); //association de chaque personnage avec son "afficheur" player.setOnCharacterChangeListener(displayPlayer); boss.setOnCharacterChangeListener(displayBoss); //initialise le combat this.combat = new Combat(); this.combat.character1 = player; this.combat.character2 = boss; this.combat.listener = this; //bouton pour demarrer le combat this.nextButton = (Button) findViewById(R.id.buttonNext); this.nextButton.setOnClickListener(this); //zone de message this.messageView = (TextView) findViewById(R.id.messageZone); } @Override public void combatTurnStart(Combat c) { Log.d("CDT", "turn " + c.currentTurn); String msg = this.getString(R.string.combat_turn_start, c.currentTurn); this.messageView.setText(msg); } @Override public void combatEnd(Combat c, Character winner) { String msg = this.getString(R.string.combat_turn_start, c.currentTurn); msg += " - " + this.getString(R.string.combat_end_winner, winner.getName()); this.messageView.setText(msg); } @Override public void combatEndExAequo(Combat c) { this.messageView.setText(R.string.combat_end_exaequo); } @Override public void onClick(View v) { this.combat.startCombat(); } }

Et voila !

Version 1.1

Cette version ajoute la notion de choix d'action au début de chaque tour. Elle ajoute aussi d'autres images pour les personnages (normal, hurt, loose, win).

On ajoute donc les fichiers d'images dans le dossier ressource.

Modification du personnage

Nouveaux états

Le personnage pourra avoir de nouveaux états. A chaque tour, le personnage qui a le moins de point de vie sera en état "hurt". S'il a gagné ou s'il a perdu, il aura l'état correspondant. Chaque état correspondra à une image.

Modification de la méthode evaluate.

public void evaluate(Character enemy){ if (! this.isAlive()){ this.currentState = "loose"; this.triggerListener(); return; } if (! enemy.isAlive()) { this.currentState = "win"; this.triggerListener(); return; } if (this.getLife() < enemy.getLife()) { this.currentState = "hurt"; this.triggerListener(); return; } this.currentState = "normal"; this.triggerListener(); }

Comme on lui ajoute un paramètre, il faut donc modifier l'appel à cette méthode (dans la classe Combat).

this.character1.evaluate(this.character2); this.character2.evaluate(this.character1);

Nouvelles images pour les nouveaux états

Pour éviter d'ajouter de trop nombreux membres pour chaque image représentant un état, on utilise un "dictionnaire".

L'idée est d'associer une chaine de caractère, "win" par exemple, à une ressource d'image, R.drawable.hero_win par exemple.

protected int normalImageResource; protected int looseImageResource; protected Map imageResources;

Il sera rempli par l'intermédiaire d'une méthode dédiée setImageResource :

public void setImageResource(String state, int imageResource) { imageResources.put(state, imageResource); }

Sans oublier de l'initialiser dans le constructeur :

imageResources = new HashMap();

Du coup, il faut modifier la méthode getImageResource pour profiter de ces nouveaux états :

public int getImageResource(){ if (this.currentState.equals("loose")){ return this.looseImageResource; } else { return this.normalImageResource; } }

public int getImageResource(){ if (this.imageResources.containsKey(this.currentState)){ return this.imageResources.get(this.currentState); } return 0; }

Les actions

Sur le même principe que les états, nous stockerons l'action en cours sous forme de chaine : "standard", "attack", "defense".

Nous ajoutons donc simplement la propriété à l'objet Character, ainsi qu'une méthode pour pouvoir la définir de l'extérieur :

protected String currentAction; public void setCurrentAction(String action){ this.currentAction = action; }

Enfin, on peut prendre en compte cette action dans le calcul des dégats infligés, et reçus :

/** * En mode standard, il recoit les degats normaux * En mode defense, il reduit les degats qu'il recoit * En mode attaque, il augmente les degats qu'il recoit car il est plus vulnerable. * * Dans tous les cas, declanche le listener * */ public void receiveDamages(int damages){ if (this.currentState == "defense"){ damages = damages / 2; } else if (this.currentState == "attack"){ damages = damages * 2; } this.currentLife = this.currentLife - damages; this.triggerListener(); } /** * En mode standard, il fait des degats normaux (1-6 PV) * En mode defense, il reduit les degats qu'il fait (1-4 PV) * En mode attaque, il augmente les degats qu'il fait (1-11 PV) * */ public int getDamages() { int maxDamages = 5; if (this.currentState == "defense"){ maxDamages = 3; } else if (this.currentState == "attack"){ maxDamages = 10; } int damages = (int) (Math.round(Math.random() * maxDamages) + 1); return damages; }

Déroulement du combat

Comme l'on souhaite demander à l'utilisateur l'action qu'il veux effectuer, il faut que le combat stoppe tant que l'utilisateur n'a pas répondu en choississant une action.

Il faut donc modifier un peu la classe Combat pour séparer le début tour de la fin.

protected void beginTurn(){ this.currentTurn++; this.listener.combatTurnStart(this); } public void finishTurn(){ this.character1.receiveDamages(this.character2.getDamages()); this.character2.receiveDamages(this.character1.getDamages()); this.character1.evaluate(this.character2); this.character2.evaluate(this.character1); if (! this.character1.isAlive() || ! this.character2.isAlive() ){ this.finishCombat(); return; } this.beginTurn(); }

L'affichage des choix d'action

On remplace le simple bouton "continuer" par ce LinearLayout

<LinearLayout android:id="@+id/toolbar" android:layout_alignParentBottom="true" android:layout_alignParentRight="true" android:padding="@dimen/basePadding" android:layout_width="wrap_content" android:layout_height="wrap_content" > <Button android:id="@+id/buttonNext" android:text="@string/start_combat" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <Button android:id="@+id/buttonActionAttack" android:text="@string/action_attack" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <Button android:id="@+id/buttonActionDefense" android:text="@string/action_defense" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <Button android:id="@+id/buttonActionStandard" android:text="@string/action_standard" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </LinearLayout>

On prévoit une nouvelle classe et une nouvelle interface pour gérer cet affichage et la réponse.

package fr.neeko.lechocdestitans; public interface OnUserChooseAction { public void actionChoosen(String action); }

package fr.neeko.lechocdestitans; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; public class UserChooseActionDisplay implements OnClickListener { public Button actionAttackButton; public Button actionDefenseButton; public Button actionStandardButton; public OnUserChooseAction listener; public void initButtons(){ actionAttackButton.setOnClickListener(this); actionDefenseButton.setOnClickListener(this); actionStandardButton.setOnClickListener(this); } public void askToUser(){ actionAttackButton.setVisibility(View.VISIBLE); actionDefenseButton.setVisibility(View.VISIBLE); actionStandardButton.setVisibility(View.VISIBLE); } public void hideFromUser(){ actionAttackButton.setVisibility(View.GONE); actionDefenseButton.setVisibility(View.GONE); actionStandardButton.setVisibility(View.GONE); } @Override public void onClick(View v) { if (v == actionAttackButton) { listener.actionChoosen("attack"); } else if (v == actionDefenseButton) { listener.actionChoosen("defense"); } else { listener.actionChoosen("standard"); } } }

Assemblage dans l'Activity

Le principe :

package fr.neeko.lechocdestitans; import android.os.Bundle; import android.app.Activity; import android.util.Log; import android.view.Menu; import android.view.View; import android.view.View.OnClickListener; import android.view.animation.AnimationUtils; import android.widget.Button; import android.widget.ImageView; import android.widget.TextView; public class GameActivity extends Activity implements CombatListener, OnClickListener, OnUserChooseAction { protected Combat combat; protected TextView messageView; protected Button nextButton; protected UserChooseActionDisplay actionDisplay; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_game); //construction du premier personnage Character player = new Character("Ryu", 20); player.setImageResource("normal", R.drawable.player_normal); player.setImageResource("hurt", R.drawable.player_hurt); player.setImageResource("loose", R.drawable.player_loose); player.setImageResource("win", R.drawable.player_win); //construction du second personnage Character boss = new Character("Zangief", 20); boss.setImageResource("normal", R.drawable.evil_normal); boss.setImageResource("hurt", R.drawable.evil_hurt); boss.setImageResource("loose", R.drawable.evil_loose); boss.setImageResource("win", R.drawable.evil_win); //construction de la classe qui afficher le premier personnage CharacterDisplay displayPlayer = new CharacterDisplay(); displayPlayer.imageView = (ImageView) findViewById(R.id.p1Portrait); displayPlayer.nameLabel = (TextView) findViewById(R.id.p1Name); displayPlayer.lifeLabel = (TextView) findViewById(R.id.p1Life); //construction de la classe qui afficher le 2e personnage CharacterDisplay displayBoss = new CharacterDisplay(); displayBoss.imageView = (ImageView) findViewById(R.id.p2Portrait); displayBoss.nameLabel = (TextView) findViewById(R.id.p2Name); displayBoss.lifeLabel = (TextView) findViewById(R.id.p2Life); //association de chaque personnage avec son "afficheur" player.setOnCharacterChangeListener(displayPlayer); boss.setOnCharacterChangeListener(displayBoss); //initialise le combat this.combat = new Combat(); this.combat.character1 = player; this.combat.character2 = boss; this.combat.listener = this; //bouton pour demarrer le combat this.nextButton = (Button) findViewById(R.id.buttonNext); this.nextButton.setOnClickListener(this); //zone de message this.messageView = (TextView) findViewById(R.id.messageZone); //initialise puis cache le "menu" this.actionDisplay = new UserChooseActionDisplay(); this.actionDisplay.actionAttackButton = (Button) findViewById(R.id.buttonActionAttack); this.actionDisplay.actionDefenseButton = (Button) findViewById(R.id.buttonActionDefense); this.actionDisplay.actionStandardButton = (Button) findViewById(R.id.buttonActionStandard); this.actionDisplay.listener = this; this.actionDisplay.initButtons(); this.actionDisplay.hideFromUser(); //affichage initial des personnages player.triggerListener(); boss.triggerListener(); } @Override public void combatTurnStart(Combat c) { Log.d("CDT", "turn " + c.currentTurn); String message = this.getString(R.string.combat_turn_start, c.currentTurn); messageView.setText(message); actionDisplay.askToUser(); nextButton.setVisibility(View.GONE); } @Override public void combatEnd(Combat c, Character winner) { String message = this.getString(R.string.combat_end_winner, winner.getName()); messageView.setText(message); } @Override public void combatEndExAequo(Combat c) { messageView.setText(R.string.combat_end_exaequo); } @Override public void onClick(View v) { this.combat.startCombat(); } @Override public void actionChoosen(String action) { this.combat.character1.setCurrentAction(action); actionDisplay.hideFromUser(); this.combat.finishTurn(); } }

Et voila ! Pas mal, non ?

Exercices et évolutions :