Silverlight 8-Ball
Publié le 19 septembre 2007
.png)
| Dans cet article, je décris comment j’ai développé un jeu de billard américain à 2 joueurs dans Silverlight (jouez ici). J’explique comment j’ai utilisé les outils Expression pour créer les éléments graphiques, et plusieurs techniques .NET pour le contrôle utilisateur et l’animation du jeu. |
Auteur : Justin Petersen |
Consulter l’article en anglais
Niveau de difficulté : Intermédiaire
Temps requis : 6 à 10 heures
Coût : Gratuit
Logiciels : Visual Studio 2008 Bêta 2, Silverlight 1.1 Alpha, Expression Design, Expression Blend 2 version préliminaire
Téléchargements : Téléchargement
Dans cet article, je décris comment j’ai développé un jeu de billard américain à 2 joueurs dans Silverlight (jouez ici). J’explique comment j’ai utilisé les outils Expression pour créer les éléments graphiques, et plusieurs techniques .NET pour le contrôle utilisateur et l’animation du jeu.
Il faut implémenter les éléments suivants pour compléter ce jeu :
- Éléments graphiques (table de billard, boule de billard, et queue de billard)
- Animation vectorielle et physique de collision
- Interaction utilisateur
- État et contrôle de jeu
.png)
Avant de poursuivre, je souhaite préciser que je ne suis pas à l’origine de l’ensemble du code et du langage XAML de cette solution. L’idée d’un jeu de billard américain Silverlight m’est venue lorsque je suis tombé sur une animation de « bulles bondissantes » 2D sur http://www.bubblemark.com/, créée par Alexey Gavrilov.
À l’origine, cette solution avait pour objectif de comparer la performance des bulles bondissantes d’une plateforme à l’autre. J’ai trouvé cette solution très utile mais je souhaitais en faire quelque chose de ludique et d’interactif.
Pour démarrer
Prérequis
J’ai utilisé les outils ci-dessous pour implémenter cette solution :
- Visual Studio 2008 Bêta 2
- Silverlight 1.1 Alpha
- Expression Design
- Expression Blend
En savoir plus sur le développement de jeux
Si vous souhaitez en savoir davantage sur le développement de jeux, j’ai trouvé plusieurs articles Coding4Fun intéressants classés sous Gaming (jeux). Plus spécifiquement, 2D Game Primer par « ZMan » présente les notions de base en détail (par ex. GameLoop, Sprites, etc.).
Éléments graphiques
Je ne suis pas vraiment un graphiste. J’ai d’ailleurs passé la plus grande partie de ma vie active à concevoir et implémenter des applications métier pour entreprises (ce qui ne nécessite pas vraiment de connaissances graphiques). Ainsi, le fait que j’ai réussi à créer rapidement des éléments graphiques pour ce jeu témoigne assez bien de l’efficacité de la suite Microsoft Expression.
Je me suis servi d’Expression Design pour créer les éléments graphiques de la table de billard. En m’inspirant des dimensions standards d’une table de billard de 3 mètres de long, j’ai dessiné la structure principale sous la forme d’un rectangle arrondi aux angles. À l’intérieur de ce rectangle, j’ai placé un rectangle vert plus petit pour la surface de jeu (en respectant les dimensions standards bien entendu). Par la suite, j’ai créé 6 cercles noirs un niveau derrière la surface de jeu en guise de poches. Enfin, j’ai ajouté la texture en bois sur le rectangle extérieur (ce qui était très simple étant donné qu’il existe un ensemble de textures en bois par défaut) et j’ai dessiné un rectangle arrondi simple en dessous de la table pour servir de barre de statut.
.png)
Par ailleurs, j’ai créé les éléments graphiques de la queue de billard à l’aide d’Expression Design. Elle est constituée de plusieurs polygones coniques et de deux demi-cercles pour l’embout et le procédé.
.png)
Ayant exporté le langage XAML associé, j’ai importé chaque élément dans mon projet. À partir de là, je pouvais voir et affiner le dessin à l’aide d’un éditeur visuel réduit ou du langage XAML associé.
.png)
.png)
Enfin, j’ai vérifié le balisage XAML pour la boule de billard issue de la solution Bubblemark d’Alexey. Il me fallait comprendre les propriétés de cet objet afin de pouvoir modifier la couleur des boules par la suite.
.png)
Animation vectorielle et physique de collision
À la base de Silverlight8Ball, une table de montage séquentiel (x:Name=”GameLoop”) sert de mécanisme d’enclenchement pour déterminer la vitesse de la boule, sa direction, et son changement de direction suite à une collision. Certaines tables de montage séquentiel sont implémentées avec des chemins et des chronologies directement prédéterminés dans XAML. Or, dans ce jeu, l’action à chaque intervalle doit être définie de façon dynamique. J’ai donc simplement paramétré la durée sur 00:00:0, et géré l’événement pour enclencher la logique de positionnement. Cela crée ce que les développeurs de jeux appellent une « boucle de jeu » (Game Loop) qui enclenche un traitement continu pour déterminer l’état de l’application.
Le gestionnaire « GameLoop_Completedhandler » qui en résulte devient l’agent de traitement racine pour l’ensemble de la logique de mouvement de la boule.
C#
1: void GameLoop_Completed(object sender, EventArgs e)
2: {
3: switch (m_ActionState)
4: {
5: case m_ActionStates.BallsMoving:
6:
7: // prep
8: List<Ball> removeList = new List<Ball>();
9: bool someBallsAreMoving = false;
10:
11: // move each ball
12: foreach (Ball ball in m_GameBalls)
13: {
14: ball.Move();
15: if (ball.InPocket) removeList.Add(ball);
16: else if (ball.IsMoving) someBallsAreMoving = true;
17: }
18:
19: // store balls sunk on shot and update ui
20: foreach (Ball ball in removeList)
21: {
22: sunkBalls.Add(ball);
23: RemoveBall(ball);
24: }
25:
26: if (someBallsAreMoving)
27: {
28: // update vectors for ball collisions
29: for (int i = 0; i < m_GameBalls.Count; i++)
30: {
31: for (int j = i + 1; j < m_GameBalls.Count; j++)
32: {
33: m_GameBalls[i].DoCollide(m_GameBalls[j]);
34: }
35: }
36: }
37: else
38: {
39: // determine shot results
40: ShotResults results = EvaluateShot();
41:
42: // apply state and ui changes
43: UpdateGameState(results);
44: }
45:
46: break;
47: }
48:
49: // restart the storyboard
50: if (m_IsRunning)
51: {
52: GameLoop.Begin();
53: }
54:
55: }
VB
1: Private Sub GameLoop_Completed(ByVal sender As Object, ByVal e As EventArgs)
2:
3: Select Case m_ActionState
4:
5: Case m_ActionStates.BallsMoving
6:
7: ' prep
8: Dim removeList As List(Of Ball) = New List(Of Ball)()
9: Dim someBallsAreMoving As Boolean = False
10:
11: ' move each ball
12: For Each ball As Ball In m_GameBalls
13: ball.Move()
14: If ball.InPocket Then
15: removeList.Add(ball)
16: ElseIf ball.IsMoving Then
17: someBallsAreMoving = True
18: End If
19: Next ball
20:
21: ' store balls sunk on shot and update ui
22: For Each ball As Ball In removeList
23: sunkBalls.Add(ball)
24: RemoveBall(ball)
25: Next ball
26:
27: If someBallsAreMoving Then
28: ' update vectors for ball collisions
29: For i As Integer = 0 To m_GameBalls.Count - 1
30: For j As Integer = i + 1 To m_GameBalls.Count - 1
31: m_GameBalls(i).DoCollide(m_GameBalls(j))
32: Next j
33: Next i
34: Else
35: ' determine shot results
36: Dim results As ShotResults = EvaluateShot()
37:
38: ' apply state and ui changes
39: UpdateGameState(results)
40: End If
41:
42: End Select
43:
44: ' restart the storyboard
45: If m_IsRunning Then
46: GameLoop.Begin()
47: End If
48:
49: End Sub
Les extraits de code ci-dessus montrent la fonctionnalité complète de notre GameLoop. Cependant, la logique principale d’animation et de collision est gérée par les lignes « ball.Move(); » et « m_GameBalls[i].DoCollide(m_GameBalls[j]); » (dans l’exemple C#). La fonction Move() de chaque boule applique son vecteur actuel (c'est-à-dire les « vitesses » x et y) pour déterminer sa prochaine position, et met à jour les coordonnées de l’élément d’interface de la boule. Une fois que la boule avance, la fonction DoCollide vérifie si deux boules se sont touchées. Si c’est le cas, leurs vecteurs sont ajustés en fonction.
C#
1: public bool DoCollide(Ball b)
2: {
3: // calculate some vectors
4: double dx = this._x - b._x;
5: double dy = this._y - b._y;
6: double dvx = this._vx - b._vx;
7: double dvy = this._vy - b._vy;
8: double distance2 = dx * dx + dy * dy;
9:
10: if (Math.Abs(dx) > this._d || Math.Abs(dy) > this._d)
11: return false;
12: if (distance2 > this._d2)
13: return false;
14:
15: // make absolutely elastic collision
16: double mag = dvx * dx + dvy * dy;
17:
18: // test that balls move towards each other
19: if (mag > 0)
20: return false;
21:
22: mag /= distance2;
23:
24: double delta_vx = dx * mag;
25: double delta_vy = dy * mag;
26:
27: this._vx -= delta_vx;
28: this._vy -= delta_vy;
29:
30: b._vx += delta_vx;
31: b._vy += delta_vy;
32:
33: return true;
34: }
VB
1: Public Function DoCollide(ByVal b As Ball) As Boolean
2: ' calculate some vectors
3: Dim dx As Double = Me._x - b._x
4: Dim dy As Double = Me._y - b._y
5: Dim dvx As Double = Me._vx - b._vx
6: Dim dvy As Double = Me._vy - b._vy
7: Dim distance2 As Double = dx * dx + dy * dy
8:
9: If Math.Abs(dx) > Me._d OrElse Math.Abs(dy) > Me._d Then
10: Return False
11: End If
12: If distance2 > Me._d2 Then
13: Return False
14: End If
15:
16: ' make absolutely elastic collision
17: Dim mag As Double = dvx * dx + dvy * dy
18:
19: ' test that balls move towards each other
20: If mag > 0 Then
21: Return False
22: End If
23:
24: mag /= distance2
25:
26: Dim delta_vx As Double = dx * mag
27: Dim delta_vy As Double = dy * mag
28:
29: Me._vx -= delta_vx
30: Me._vy -= delta_vy
31:
32: b._vx += delta_vx
33: b._vy += delta_vy
34:
35: Return True
36: End Function
Interaction utilisateur
Bien que la table de montage GameLoop gère bien le mouvement des boules, ce n’était pas selon moi la meilleure option pour gérer le contrôle de la queue de billard et de la boule blanche. Au moment du contrôle utilisateur (boule blanche pointée et queue de billard en main), j’ai décidé d’utiliser le mouvement de la souris et les événements de clic pour déterminer la position et l’angle de l’objet. J’ai créé ma propre énumération pour gérer le passage entre les états de mouvement de boule et de contrôle utilisateur. Le reste nécessite quelques statistiques et transformations XAML Silverlight simples.
Boule blanche pointée et queue de billard en main
Lors d’un coup, l’événement MouseMove permet de déplacer la boule blanche. Lors du pointage, il permet à l’utilisateur de faire pivoter la queue de billard autour de la boule blanche.
C#
1: void Page_MouseMove(object sender, MouseEventArgs e)
2: {
3:
4: m_MousePoint = e.GetPosition(this);
5:
6: switch (m_ActionState)
7: {
8: case m_ActionStates.Scratch:
9: m_QBall.MoveAbsolute(m_MousePoint.X, m_MousePoint.Y);
10: break;
11: case m_ActionStates.Aiming:
12: m_PoolStick.Rotate(m_MousePoint.X, m_MousePoint.Y, m_QBall.BallCenterX, m_QBall.BallCenterY);
13: break;
14: }
15:
16: }
VB
1: Private Sub Page_MouseMove(ByVal sender As Object, ByVal e As MouseEventArgs)
2:
3: m_MousePoint = e.GetPosition(Me)
4:
5: Select Case m_ActionState
6: Case m_ActionStates.Scratch
7: m_QBall.MoveAbsolute(m_MousePoint.X, m_MousePoint.Y)
8: Case m_ActionStates.Aiming
9: m_PoolStick.Rotate(m_MousePoint.X, m_MousePoint.Y, m_QBall.BallCenterX, m_QBall.BallCenterY)
10: End Select
11:
12: End Sub
La puissance de la méthode RotateTransform
La méthode RotateTranform de Silverlight est très puissante dans ce cas. Elle me permet simplement de faire pivoter la queue de billard autour d’un point central (le centre de la boule blanche) à partir d’un angle relatif entre le pointeur de la souris et la boule.
C#
1: public void Rotate (double mouseX, double mouseY, double ballX, double ballY)
2: {
3: double vx = ballX - mouseX;
4: double vy = ballY - mouseY;
5:
6: radians = Math.Atan2(vy, vx);
7: double angle = radians * (180/Math.PI);
8:
9: rootCanvas.RenderTransform = new RotateTransform
10: {
11: CenterX = Model.stickBuffer,
12: CenterY = Model.stickHeight/2,
13: Angle = angle
14: };
15:
16: }
VB
1: Public Sub Rotate(ByVal mouseX As Double, ByVal mouseY As Double, ByVal ballX As Double, ByVal ballY As Double)
2: Dim vx As Double = ballX - mouseX
3: Dim vy As Double = ballY - mouseY
4:
5: radians = Math.Atan2(vy, vx)
6: Dim angle As Double = radians * (180 / Math.PI)
7:
8: Dim transform As RotateTransform = New RotateTransform()
9: transform.CenterX = Model.stickBuffer
10: transform.CenterY = Model.stickHeight / 2
11: transform.Angle = angle
12:
13: rootCanvas.RenderTransform = transform
14:
15: End Sub
Contrôle de la puissance et placement de la boule blanche
En fonction d’ActionState, les événements souris relâchée et souris appuyée gèrent l’interaction utilisateur lors de l’ajustement de la puissance et du placement de la boule blanche.
C#
1: void Page_MouseLeftButtonUp(object sender, MouseEventArgs e)
2: {
3: if (this.m_ActionState == m_ActionStates.AdjustingPower)
4: {
5: m_QBall.Strike(m_PoolStick.power, m_PoolStick.radians);
6:
7: m_PoolStick.StopPowerMovement();
8: this.Children.Remove(m_PoolStick);
9: this.m_ActionState = m_ActionStates.BallsMoving;
10: }
11: }
12:
13: void Page_MouseLeftButtonDown(object sender, MouseEventArgs e)
14: {
15: switch (m_ActionState)
16: {
17: case m_ActionStates.Aiming:
18: m_PoolStick.StartPowerMovement();
19: this.m_ActionState = m_ActionStates.AdjustingPower;
20: break;
21: case m_ActionStates.Scratch:
22: ResetPoolStick();
23: m_ActionState = m_ActionStates.Aiming;
24: break;
25: }
26: }
VB
1: Private Sub Page_MouseLeftButtonUp(ByVal sender As Object, ByVal e As MouseEventArgs)
2: If Me.m_ActionState = m_ActionStates.AdjustingPower Then
3: m_QBall.Strike(m_PoolStick.power, m_PoolStick.radians)
4:
5: m_PoolStick.StopPowerMovement()
6: Me.Children.Remove(m_PoolStick)
7: Me.m_ActionState = m_ActionStates.BallsMoving
8: End If
9: End Sub
10:
11: Private Sub Page_MouseLeftButtonDown(ByVal sender As Object, ByVal e As MouseEventArgs)
12: Select Case m_ActionState
13: Case m_ActionStates.Aiming
14: m_PoolStick.StartPowerMovement()
15: Me.m_ActionState = m_ActionStates.AdjustingPower
16: Case m_ActionStates.Scratch
17: ResetPoolStick()
18: m_ActionState = m_ActionStates.Aiming
19: End Select
20: End Sub
État et contrôle de jeu
Les « règles métier » du jeu constituent le dernier composant de cette application. Un jeu de billard américain est régi par l’issue du tour de chaque joueur. Ce sont les boules entrées dans les poches qui définissent si le jeu est terminé ou quel est le prochain joueur.
J’ai créé une autre énumération pour ShotResults qui définit chaque résultat potentiel : GoAgain, NextPlayer, Scratch, ScratchOnEight, PrematureEightBall et Win.
C#
1: private void UpdateGameState(ShotResults results)
2: {
3: string resultText = "";
4:
5: switch (results)
6: {
7: // update game states
8: case ShotResults.PrematureEightBall:
9: case ShotResults.ScratchOnEight:
10: resultText = ProcessGameEnd(false, results);
11: m_ActionState = m_ActionStates.GameOver;
12: break;
13: case ShotResults.NextPlayer:
14: resultText = "Player looses turn";
15: ChangeCurrentPlayer();
16: ResetPoolStick();
17: m_ActionState = m_ActionStates.Aiming;
18: break;
19: case ShotResults.GoAgain:
20: resultText = "Nice job.Go again.";
21: ResetPoolStick();
22: m_ActionState = m_ActionStates.Aiming;
23: break;
24: case ShotResults.Scratch:
25: resultText = "Scratch.Player looses turn.";
26: ChangeCurrentPlayer();
27: AddBall(m_QBall);
28: m_ActionState = m_ActionStates.Scratch;
29: break;
30: case ShotResults.Win:
31: resultText = ProcessGameEnd(true, results);
32: m_ActionState = m_ActionStates.GameOver;
33: break;
34: }
35:
36: sunkBalls.Clear();
37:
38: this.Text_Status.Text = resultText;
39: }
VB
1: Private Sub UpdateGameState(ByVal results As ShotResults)
2: Dim resultText As String = ""
3:
4: Select Case results
5: ' update game states
6: Case ShotResults.PrematureEightBall, ShotResults.ScratchOnEight
7: resultText = ProcessGameEnd(False, results)
8: m_ActionState = m_ActionStates.GameOver
9: Case ShotResults.NextPlayer
10: resultText = "Player looses turn"
11: ChangeCurrentPlayer()
12: ResetPoolStick()
13: m_ActionState = m_ActionStates.Aiming
14: Case ShotResults.GoAgain
15: resultText = "Nice job.Go again."
16: ResetPoolStick()
17: m_ActionState = m_ActionStates.Aiming
18: Case ShotResults.Scratch
19: resultText = "Scratch.Player looses turn."
20: ChangeCurrentPlayer()
21: AddBall(m_QBall)
22: m_ActionState = m_ActionStates.Scratch
23: Case ShotResults.Win
24: resultText = ProcessGameEnd(True, results)
25: m_ActionState = m_ActionStates.GameOver
26: End Select
27:
28: sunkBalls.Clear()
29:
30: Me.Text_Status.Text = resultText
31: End Sub
Conclusion
Cet exercice était à la fois ludique et éducatif (ce qui est toujours une bonne combinaison). J’ai appris assez de choses en matière de conception graphique pour ne plus en avoir peur, surtout à l’aide de la suite Expression. Cet exercice m’a également permis de comprendre comment Silverlight nous permettra d’implémenter des applications interactives plus riches. Cependant, la partie la plus intéressante pour moi était mon changement de perspective dans l’implémentation d’un jeu « toujours en marche ». Aussi simple que soit un jeu de billard américain, j’ai dû adopter une nouvelle façon de penser par rapport au modèle de programmation fondé sur les événements auquel je suis habitué au quotidien.
Si vous avez des questions sur cette solution ou que vous avez des suggestions à faire pour la perfectionner, contactez-moi à l’adresse jpetersen@claritycon.com.