CHAPITRE3APPLICATIONSGRAPHIQUES
AlexandreBlondinMasséDépartementd'informatique
UniversitéduQuébecàMontréal30septembre2015
INF5071-Infographie
TWEENSDansuneanimation,l'unitéprimitiveestappeléetween(del'anglais,between);Essentiellement,ils'agitdetransformerunobjetd'unétate1versunétate2;Ils'agitalorssimplementdedécrirelesélémentssuivants:
Quelestl'étatinitial?Quelestl'étatfinal?Dequellefaçonprogresse-t-ondel'étatinitialàl'étatfinal?Est-cequel'animationdoitêtrerépétée?Doit-elleêtrerejouéeàl'envers?
TWEENENPHASER
1. Oncréed'aborduntweenenluiassignantunesprite:
2. Puisonindique,àl'aidedelafonctionto,ceenquoilaspritedoitêtretransformée:
3. Plusieursoptionssontdisponibles:
4. Enplusdespécifierlesattributscibles,ilestégalementpossibledespécifierlesattributsd'origineàl'aidedelaméthodefrom.
var cloud = game.add.sprite(200, 100, 'cloud');var tween = game.add.tween(cloud);
// On se déplace vers x = 650 en 3000 millisecondestween.to({x: 650}, 3000);
// Pour faire le mouvement inverse une fois la cible atteintetween.yoyo(true);// Pour répéter indéfinimenttween.repeat(-1);// Pour répéter indéfiniment (2e façon)tween.loop(true);
DÉPLACEMENTSDifférentstweensd'unnuage:
var game = new Phaser.Game(800, 600, Phaser.AUTO, 'game', {preload: preload, create: create, update: update});
function preload() { game.load.image('cloud', 'assets/cloud.png');}
function create() { var style = {fill: "#fff"}; game.add.text(5, 100, 'simple', style); game.add.text(5, 200, 'yoyo', style); game.add.text(5, 300, 'loop', style); game.add.text(5, 400, 'yoyo+repeat', style); var cloud1 = game.add.sprite(200, 100, 'cloud'); var cloud2 = game.add.sprite(200, 200, 'cloud'); var cloud3 = game.add.sprite(200, 300, 'cloud'); var cloud4 = game.add.sprite(200, 400, 'cloud'); var tween1 = game.add.tween(cloud1); tween1.to({x: 650}, 3000); // Durée = 3000 ms tween1.start(); var tween2 = game.add.tween(cloud2); tween2.to({x: 650}, 3000); tween2.yoyo(true); tween2.start(); var tween3 = game.add.tween(cloud3); tween3.to({x: 650}, 3000); tween3.loop(true); tween3.start(); var tween4 = game.add.tween(cloud4); tween4.to({x: 650}, 3000); tween4.yoyo(true);
Voirlerésultat
AUTRESPROPRIÉTÉSCequiestparticulièrementintéressant,c'estqu'onpeutconstruireuntweenpourn'importequellepropriétédelasprite.Parexemple,celafonctionneavec:
Latransparence(alpha);L'angle(angleourotation);Lagrandeur(scale);etc.
Ilestmêmepossibledecréersespropresattributspourlasprite.
PROGRESSION(EASING)Lorsqu'unobjetprogressed'unétatàunautre,pardéfaut,laprogressionestlinéaire;Ilesttrèssimple,enPhaser,demodifiercecomportement,enpassantenparamètreunefonctionquidécritlaprogression;Phaseroffredirectementplusieursfonctions:
Quadratique;Exponentielle;Avecrebonds;etc.
Ilestégalementpossiblededéfinirsespropresfonctions.
EXEMPLEDEPROGRESSIONvar game = new Phaser.Game(800, 800, Phaser.AUTO, 'game', {preload: preload, create: create, update: update});
function preload() { game.load.image('cloud', 'assets/cloud.png');}
function create() { var style = {fill: "#fff"}; game.add.text(5, 50, 'back-in', style); game.add.text(5, 150, 'back-out', style); game.add.text(5, 250, 'back-in-out', style); game.add.text(5, 350, 'bounce-out', style); game.add.text(5, 450, 'circular-out', style); game.add.text(5, 550, 'sinus-in-out', style); game.add.text(5, 650, 'exponential-in', style); var cloud1 = game.add.sprite(200, 50, 'cloud'); var cloud2 = game.add.sprite(200, 150, 'cloud'); var cloud3 = game.add.sprite(200, 250, 'cloud'); var cloud4 = game.add.sprite(200, 350, 'cloud'); var cloud5 = game.add.sprite(200, 450, 'cloud'); var cloud6 = game.add.sprite(200, 550, 'cloud'); var cloud7 = game.add.sprite(200, 650, 'cloud'); // Back-in var tween1 = game.add.tween(cloud1); tween1.to({x: 650}, 3000); // Durée = 3000 ms tween1.easing(Phaser.Easing.Back.In); tween1.repeat(-1); tween1.start(); // Back-out var tween2 = game.add.tween(cloud2); tween2.to({x: 650}, 3000); // Durée = 3000 ms tween2.easing(Phaser.Easing.Back.Out); tween2.repeat(-1);
Voirlerésultat
REPRÉSENTATIONDESPROGRESSIONS
Mathématiquement,lesprogressionssontsimplementreprésentéespardesfonctions:
f:[0, d] → ℝ
oùdestladuréedel'animation.
Parexemple,considéronslaprogressionlinéairequipassedex = 100àx = 400en3000millisecondes.
Alorslafonctionquireprésentecetteprogressionest
f:[0, 3000] → ℝ
définiepar
f(t) = 100(1 − t ⁄ 3000) + 400t ⁄ 3000
FEUILLESDEPOLICESLorsqu'onsouhaiteanimerdutexte,parexemple,ilestdifficile(oumêmeimpossible)delefaireavecdel'affichagedetexteconventionnel;Onrecourtdoncàdesfeuillesdespritesdanslesquelleslesspritessontjustementdeschiffresetdeslettres.
EXEMPLE(GAMEOVER)Avectrèspeud'efforts,onpeutfairedesanimationsassez
complexes.var game = new Phaser.Game(800, 600, Phaser.AUTO, 'game', {preload: preload, create: create, update: update});
function preload() { game.load.spritesheet('fonts', 'assets/fonts.jpeg', 37, 37);}
function create() { game.stage.backgroundColor = 0xFFFFFF; var word = "game over"; for (var i = 0; i < word.length; ++i) { if (word[i] != ' ') { // On récupère la sprite de la lettre correspondante // La largeur de chaque lettre est 37 // La hauteur initiale est -100 pour ne pas voir les lettres var letter = game.add.sprite(i * 37 + 50, -100, 'fonts', word.charCodeAt(i) - 97); // Puis on ajoute l'animation avec rebonds (Bounce.Out) // Le premier "true" signifie que l'animation est lancée automatiquement // Le délai avant de commencer est 50 * i (pour créer un décalage) game.add.tween(letter).to({y: 200}, 2000, Phaser.Easing.Bounce.Out, true, 50 * i); } }}
function update() {}
Voirlerésultat
COURBESDEBÉZIERNousnousintéressonsmaintenantauxcourbesendimension2,etplusprécisémentauxcourbesdeBézier;
Ilexisteplusieursfaçonsdedécriremathématiquementunecourbe;
L'uned'entreellesconsisteàutiliseruneparamétrisation
P:ℝ → ℝ2:P(t) = (x(t), y(t))
ouparfois
P:[0, 1] → ℝ2:P(t) = (x(t), y(t))
Leparamètretpeutêtrevucommeletemps,alorsquex(t)ety(t)sontlesabscissesetordonnéesenfonctiondutempst.
PARAMÉTRISATIOND'UNSEGMENT
Lacourbelaplussimpleestsansaucundoutelesegmentdedroite;
SupposonsqueP0etP1soientlesextrémitésdusegmentetqu'onsouhaitesedéplacerdeP1versP2;
Alorsuneparamétrisationpossibleest
P:[0, 1] → ℝ2:P(t) = (1 − t)P0 + tP1
ExempleavecP0 = (2, 1)etP1 = (7, 3),etalorsP(t) = (1 − t)(2, 1) − t(7, 3):
EXERCICE1. Donnezlaparamétrisationd'unecourbequidécritunarcdecercle,
encommençantaupoint(100, 100),terminantaupoint(200, 200)etselonlecercledecentre(100, 200)etderayon100.Laduréetotaledelatrajectoiredoitêtrede3000millisecondes.
2. Donnezlaparamétrisationd'unecourbeelliptiquequiévolueautourdel'ellipsedecentre(300, 300)etderayons200et100,danslesenshoraire,àpartird'unpointquelconquedel'ellipse.
EXEMPLE
DansPhaser,onpeututilisertroistypesdeparamétrisation:
Interpolationlinéaire;InterpolationdeBézier;InterpolationdeCatmull-Rom.
Onsefixeuncertainnombredepoints,parexemple
(w, 0), (w, h), (0, h), (0, 0)
oùwethsontrespectivementlalargeuretlahauteurducanvas,moinslesdimensionsdel'image(pourqu'ellesoitexactementàlafrontière).
Voirl'exemple
COURBEDEBÉZIERCUBIQUEOnaquatrepointsdecontrôleP0,P1,P2etP3;L'ordreestimportant;OncommenceaupointP0etontermineaupointP3;Généralement,onnepassepasparlespointsP1etP2;
(source:Wikipedia.org)
PROPRIÉTÉSDESCOURBESDEBÉZIER
LacourbedeBézierestunsegmentexactementlorsquelespointsdecontrôlesontalignés.
Unarcdecercleouuncerclen'estpasunecourbedeBézier(ilfautl'approximer);
Toutetransformationaffined'unecourbedeBézierestéquivalenteàappliquerlatransformationaffinesurchacundespointsdecontrôle.
Laformulepourlaparamétrisationd'unecourbedeBéziercubiqueest
P(t) = P0(1 − t)3 + 3P1t(1 − t)2 + 3P2t2(1 − t) + P3t3
INTERACTIONETÉVÉNEMENTS
Lorsqu'onconçoituneapplicationgraphique,ilestimportantdepouvoirinteragiravecl'utilisateur;Ilyadifférentsmécanismesquipermettentd'intervenir:
Leclavier;Lasouris;Unpavétactile;Unstylet,etc.
DansPhaser,lesobjetsdetypePointersontunereprésentationnormaliséedesdifférentsscénarios.
BOUTONSIlesttrèssimpledecréerunboutondansPhaser;Onpeutmêmepasserunefeuilledespritesquipermetdemodifierl'apparenceselonl'étatdanslequelonsetrouve:
over:au-dessusdubouton;down:leboutonestenfoncé;up:leboutonvientd'êtrerelâché;out:lepointeursetrouveàl'extérieurdubouton.
EXEMPLELeboutonaétéfaitàl'aided'Inkscape;Lorsqu'onclique,unnombrealéatoireapparaît.
var game = new Phaser.Game(800, 600, Phaser.AUTO, 'game', {preload: preload, create: create});var button;var text;
function preload() { game.load.spritesheet('button', 'assets/bouton.png', 300, 160);}
function create() { game.stage.backgroundColor = 0xFFFFFF; button = game.add.button(game.world.centerX - 150, 400, 'button', actionOnClick, this, 2, 1, 0, 2); button.onInputOver.add(over, this); button.onInputOut.add(out, this); button.onInputUp.add(up, this); text = game.add.text(game.world.centerX, 200, Math.random()); text.anchor.setTo(0.5, 0.5);}
function up() { console.log('button up', arguments);}
function over() { console.log('button over');}
function out() {
Voirlerésultat
ACTIVER/STOPPERUNEANIMATION
Ilesttrèssimpledemettresurpauseuneanimation:ilsuffitdemodifierl'attributgame.pausedàtrueoufalse;
Lorsqu'oncliquesurlecanvas,peuimportelaposition,ilsuffitd'utiliserlafonctiongame.input.onDown.add:
// Lorsqu'on clique on change l'étatgame.input.onDown.add(onDown);function onDown() { game.paused = !game.paused;};
Voirlerésultat
GRILLEHEXAGONALE(1/2)
Voiciunepetiteapplicationdanslaquelleonsélectionnedescellulesdansunegrillehexagonale;
Onutiliselafeuilledespritessuivante:
Lorsqu'oncliquesurunecellule,onsouhaitequ'elledemeureencyan;
Lorsqu'onestau-dessusd'unecellule,onsouhaitequ'elleapparaisseenjaunepâle.
GRILLEHEXAGONALE(2/2)var game = new Phaser.Game(800, 600, Phaser.AUTO, 'game', {preload: preload, create: create, update: update});
function preload() { game.load.spritesheet('hexagon', 'assets/hexagone.png', 82, 71);}
function create() { game.stage.backgroundColor = 0xFFFFFF;
for (var i = -8; i < 8; ++i) { for (var j = -8; j < 8; ++j) { var button; button = game.add.button(60.5 * i + game.world.centerX / 2, -34.5 * i + 200 + j * 71 + game.world.centerY / 2, 'hexagon', onClick, this, 1, 0, 2, 0); var onClick = function(button) { button.freezeFrames = !button.freezeFrames; } } }}
function update() {}
Voirlerésultat
CLAVIER
Phaserpermetdetenircomptedesentréesauclaviertrèssimplement;
Ilsuffitd'aborddecréerunobjetpartouche:
Puisonpeutluiassocierdesévénements,quisontdesfonctions:
space = game.input.keyboard.addKey(Phaser.Keyboard.SPACEBAR);
space.onDown.add(onDown, this);function onDown() { game.paused = !game.paused;};
PAUSEAVECCLAVIERvar game = new Phaser.Game(800, 600, Phaser.AUTO, 'game', {preload: preload, create: create, update: update});var bird;
function preload() { game.load.image('background', 'assets/background.png'); game.load.image('rock', 'assets/rock.png'); game.load.image('cloud', 'assets/cloud.png'); game.load.spritesheet('bird', 'assets/bird.png', 240, 314);}
function create() { game.add.sprite(0, 0, 'background'); game.add.sprite(400,500, 'rock'); game.add.sprite(400,450, 'rock'); game.add.sprite(400,400, 'rock'); var cloud = game.add.sprite(0,200, 'cloud'); bird = game.add.sprite(400, 200, 'bird'); bird.scale.setTo(0.4, 0.4); game.physics.startSystem(Phaser.Physics.ARCADE); game.physics.arcade.enable(cloud); game.physics.arcade.enable(bird); cloud.enableBody = true; cloud.body.velocity.x = 200; cloud.body.bounce.x = 1.0; cloud.body.collideWorldBounds = true; bird.animations.add('fly'); bird.animations.play('fly', 30, true); // 50 IPS, boucle infinie = vrai bird.enableBody = true; bird.body.velocity.x = -100;
// Lorsqu'on appuie sur espace, on change l'état space = game.input.keyboard.addKey(Phaser.Keyboard.SPACEBAR); space.onDown.add(onDown, this);
Voirlerésultat
STRUCTURED'UNEAPPLICATION
Jusqu'àmaintenant,nousn'avonspasdiscutéd'unefaçonpropredediviserlecoded'uneapplicationgraphique;Phaserproposeunestructuredebasequipermetcela;Ils'agitd'utiliserlegestionnaired'états(StateManager);Unexempled'applicationcomplètesetrouveici
LEFICHIERHTML<!DOCTYPE HTML><html><head> <meta charset="UTF-8" /> <title>Structure de base d'un projet Phaser</title> <script src="js/phaser.min.js"></script> <script src="js/boot.js"></script> <script src="js/preloader.js"></script> <script src="js/menu.js"></script> <script src="js/game.js"></script></head><body>
<div id="game"></div>
<script type="text/javascript">window.onload = function() { // L'application var game = new Phaser.Game(800, 600, Phaser.AUTO, 'game');
// Les états possibles game.state.add('boot', MyGame.Boot); game.state.add('preloader', MyGame.Preloader); game.state.add('menu', MyGame.Menu); game.state.add('game', MyGame.Game);
// On démarre avec l'état 'boot' game.state.start('boot');};</script>
</body></html>
LEDÉMARRAGEDEL'APPLICATION/* Le singleton qui contiendra tout ce qui est relatif à cette application. En principe, il devrait s'agir de l'UNIQUE variable globale de l'application. */
var MyGame = {};
// Le constructeur de l'objet BootMyGame.Boot = function (game) {};
// Les méthodes de l'objet BootMyGame.Boot.prototype = { init: function () { // Si votre application tient compte des pointeurs multiples ou non // Concerne par exemple les appareils avec écran tactile qui tiennent // compte des événements déclenchés lorsqu'on utilise plusieurs doigts this.input.maxPointers = 1;
// Arrière-plan this.stage.backgroundColor = 0xFFFFFF;
// Pour économiser de l'énergie si l'application n'a pas le 'focus' this.stage.disableVisibilityChange = true;
if (this.game.device.desktop) { // Éléments spécifiques aux applications sur 'Desktop' this.scale.pageAlignHorizontally = true; } else { // Éléments spécifiques aux applications sur 'mobiles' this.scale.scaleMode = Phaser.ScaleManager.SHOW_ALL; this.scale.setMinMax(400, 300, 800, 600); this.scale.forceLandscape = true; this.scale.pageAlignHorizontally = true; } },
LEPRÉCHARGEMENTDESRESSOURCESMyGame.Preloader = function(game) { this.preloadBar = null; this.ready = false;};
MyGame.Preloader.prototype = { preload: function () { // On ajoute la barre de progression this.preloadBar = this.add.sprite(this.world.centerX, this.world.centerY, 'progression-bar'); this.preloadBar.anchor.setTo(0.5, 0.5);
// On indique que la barre de progression affiche effectivement la // progression // En réalité, l'image est "coupée" selon la proportion de ressources // qui ont été chargées this.load.setPreloadSprite(this.preloadBar);
// Puis on fait le chargement de toutes les ressources nécessaires this.load.image('background', 'assets/background.png'); this.load.image('rock', 'assets/rock.png'); this.game.load.image('cloud', 'assets/cloud.png'); this.load.spritesheet('bird', 'assets/bird.png', 240, 314); this.load.spritesheet('play-button', 'assets/bouton.png', 300, 160); this.load.audio('main-theme', ['assets/main-theme.mp3']); this.load.spritesheet('fonts', 'assets/fonts.jpeg', 37, 37); },
create: function () { // Lorsque le préchargement est terminé, on affiche la barre complète this.preloadBar.cropEnabled = false; },
update: function () { // Disons qu'il y a de la musique
LEMENUMyGame.Menu = function (game) { this.music = null; this.playButton = null;};
MyGame.Menu.prototype = { create: function () { // On démarre la musique this.music = this.add.audio('main-theme'); this.music.play(); this.playButton = this.add.button(this.world.centerX, this.world.centerY, 'play-button', this.startGame, this, 2, 1, 0, 2); this.playButton.anchor.setTo(0.5, 0.5); },
update: function () { // Normalement, il y a quelque chose ici },
startGame: function (pointer) { // On arrête la musique avant de démarrer le jeu this.music.stop(); // Puis on démarre ! this.state.start('game'); }};
LAPARTIEPRINCIPALEMyGame.Game = function (game) { this.game; // a reference to the currently running game (Phaser.Game) this.add; // used to add sprites, text, groups, etc (Phaser.GameObjectFactory) this.camera; // a reference to the game camera (Phaser.Camera) this.cache; // the game cache (Phaser.Cache) this.input; // the global input manager. You can access this.input.keyboard, this.input.mouse, as well from it. (Phaser.Input) this.load; // for preloading assets (Phaser.Loader) this.math; // lots of useful common math operations (Phaser.Math) this.sound; // the sound manager - add a sound, play one, set-up markers, etc (Phaser.SoundManager) this.stage; // the game stage (Phaser.Stage) this.time; // the clock (Phaser.Time) this.tweens; // the tween manager (Phaser.TweenManager) this.state; // the state manager (Phaser.StateManager) this.world; // the game world (Phaser.World) this.particles; // the particle manager (Phaser.Particles) this.physics; // the physics manager (Phaser.Physics) this.rnd; // the repeatable random number generator (Phaser.RandomDataGenerator)};
MyGame.Game.prototype = { create: function () { var word = "game over"; for (var i = 0; i < word.length; ++i) { if (word[i] != ' ') { // On récupère la sprite de la lettre correspondante // La largeur de chaque lettre est 37 // La hauteur initiale est -100 pour ne pas voir les lettres var letter = this.game.add.sprite(i * 37 + 50, -100, 'fonts', word.charCodeAt(i) - 97); // Puis on ajoute l'animation avec rebonds (Bounce.Out) // Le premier "true" signifie que l'animation est lancée automatiquement // Le délai avant de commencer est 50 * i (pour créer un décalage) this.game.add.tween(letter).to({y: 200}, 2000, Phaser.Easing.Bounce.Out, true, 50 * i); } }