Silverlight 8-Ball
 | In this article, I will discuss how I have built a 2 player 8-Ball game in Silverlight (play here). I will explain how I used the Expression tools to design the graphics and various .NET techniques to enable user control and game animation. |
| Justin Petersen Difficulty: Intermediate Time Required: 6-10 hours Cost: Free Hardware: |
In this article, I will discuss how I have built a 2 player 8-Ball game in Silverlight (play here). I will explain how I used the Expression tools to design the graphics and various .NET techniques to enable user control and game animation.
The following components must be implemented to complete this game:
- Graphics (pool table, pool ball, and pool stick)
- Vector animation and collision physics
- User interaction
- Game state and control
Before I continue, I’d like to point out that I did not originate all of the code and XAML for this solution. I came up with the idea of creating a Silverlight 8-Ball game after stumbling upon a 2-D “bouncing bubble” animation here: http://www.bubblemark.com/ created by Alexey Gavrilov.
The original intent for this solution was to compare the performance of the bouncing bubbles across various platforms. I found this very useful, but was personally interested in making something fun and interactive out of it.
Getting Started
Prerequisites
The following tools were used to implement this solution:
- Visual Studio 2008 Beta 2
- Silverlight 1.1 Alpha Refresh
- Expression Design
- Expression Blend
Learning more about game development
If you'd like to learn more about game development, I found a number Coding4Fun posts under Gaming helpful. In particular, the 2D Game Primer by "ZMan" gives a good overview of the basics (e.g. GameLoop, Sprites, etc.)
Graphics
I am not much of a graphic designer. In fact, I've spent the majority of my career designing and implementing line of business apps for enterprise customers (typically not a very graphically intensive task). So I believe it is a decent testament to the effectiveness of the Microsoft Expression suite that I could, in short order, create the graphics for this game.
I used Expression Design to build the graphics of the pool table. Modeling my design after the standard dimension of a 9 foot billiards table, I drew the main structure as a rectangle with rounded corners. I drew a smaller green rectangle centered over it for the playing surface (of course following standard dimensions). Next, I created 6 black circles placed one level behind the playing surface to give the impression of pockets. Lastly, I added the wood texture to the outside rectangle (which was super easy as there is a set of wood textures available by default), and drew a simple rounded rectangle beneath the table to serve as a status bar.
I also created the graphics for my pool stick using Expression Design. This consisted of a few tapered polygons and 2 half circles for the butt and tip.
After exporting the associated XAML, I imported each element into my project. From here, I could view and refine the design through a scaled down visual editor or the associated XAML.
Lastly, I inspected the XAML markup for the pool ball I reused from Alexey's Bubblemark solution. I needed to understand the properties of this object so that I could modify the color of the balls later on.
Vector Animation and Collision Physics
The root of Silverlight8Ball is a Storyboard (x:Name=”GameLoop”) that acts as a triggering mechanism for determining ball velocity, direction, and redirection due to collision. Whereas some Storyboards are implemented with predetermined paths and timelines directly in XAML, the action taken at each tick must be determined dynamically in this game. So we simply set the duration to 00:00:0, and handle the Completed event to trigger our positioning logic. This design effectively creates what game developers call a “Game Loop” that triggers continuous processing to determine application state.
The resulting “GameLoop_Completedhandler” becomes the root processing agent for all of the game’s ball movement logic.
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
The above code snippets demonstrate the full functionality of our GameLoop. However, the primary animation and collision logic are handled by "ball.Move();" and "m_GameBalls[i].DoCollide(m_GameBalls[j]);" lines (in C# example). The Move() function of each ball applies its current vector (i.e. x and y "velocities") to determine its next position and updates the ball's UI element coordinates. Once the ball is advanced, the DoCollide function checks if two balls have collided. If so, their vectors are adjusted accordingly.
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
User Interaction
While the GameLoop Storyboard does a good job at managing ball movement, I didn't think it was the best option for handling pool stick and q-ball control. During times of user control (stick aiming and q-ball "ball in hand"), I chose to use mouse movement and click events to determine object position and angle. I created my own enumeration to manage switching between ball movement and user control states. The rest involves some simple statistics and Silverlight XAML transformations.
Pool Stick Aiming and Ball-In-Hand
During a scratch, the MouseMove event allows the affect of moving the q-ball for placement. During aiming, it allows the user to rotate the poolstick around the q-ball.
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
The RotateTransform Rocks
The Silverlight RotateTranform is very powerful in this case. It allows me to simply rotate the pool stick object around a center point (the center of the q-ball) using a relative angle between the mouse point and the ball.
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
Power Control and Q-Ball Placement
Based on the ActionState, the mouse up and mouse down events manage user interaction during power adjustment and q-ball placement.
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
Game State and Control
The final component to this application is the "business rules" behind the game. A game of 8-Ball is governed by the results of each player's shot. Whether that shot ends the game or whose shot it is next is based on what balls are sunk.
I created another enumeration for ShotResults which defines each potential outcome: GoAgain, NextPlayer, Scratch, ScratchOnEight, PrematureEightBall, and 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
This was a fun and educational exercise for me (always a good combination). I learned enough about graphics design to at least fear it less, especially with the use of the Expression suite. It also opened my perspective of Silverlight to understand how it will empower us to implement more rich interactive applications. The most interesting part for me, however, was the change in perspective I had to take in implementing an "always on" game. As simple as a game of 8-ball is, it definitely required an adjustment in thinking from the event-driven model of programming I'm accustomed to using in my every day work.
If you have any questions about this solution or have suggestions for improvement, feel free to contact me at jpetersen@claritycon.com