帮酷LOGO
  • 显示原文与译文双语对照的内容
文章标签:PONG  Silverlight  


 silverlight_pong.jpg

介绍

本文介绍了如何使用Silverlight编写游戏。 它解释了制作游戏所需的机制,如何实现它们,并展示它们应用于完整游戏的机制。

游戏就像( 但完全实现) 一样简单。 这让我们可以浏览完整的代码并了解它是如何工作的。 为了让文章能够专注于代码访问Silverlight框架和游戏基本机制,我故意没有编写任何sprite框架或者 helper 函数。

目标是在阅读本文之后,你将知道:

  • 做游戏需要做什么
  • 你可以调用Silverlight来实现这里游戏的功能

背景

我在本文后面的想法是看一下使用Silverlight是否可以快速编写一个简单的游戏,如我在/4a或者我的BBC模型中所做的。 在 80年,可以能会在几小时和几行代码中编写简单的游戏,比如,。

我决定编写最简单的游戏: 只是一个球拍和一个球,尝试在 80风格的( 。例如,没有面向对象的编码) 中编写代码。

这个实验的结论是,Silverlight提供了一个允许编写游戏非常容易的平台。 完整的游戏源代码少于 200行 C# 和 50行的XAML 。

使用代码

这是完整的代码。 完全游戏没有那么长的时间是吧?

using System;using System.Collections.Generic;using System.Linq;using System.Net;using System.Windows;using System.Windows.Controls;using System.Windows.Documents;using System.Windows.Input;using System.Windows.Media;using System.Windows.Media.Animation;using System.Windows.Shapes;namespace SpriteDemo1
{
 publicpartialclass MainPage : UserControl
 {
 enum GameState { RUNNING, PAUSED, GAMEOVER };
 privatedouble ballPosX;
 privatedouble ballSpeedX;
 privatedouble ballPosY;
 privatedouble ballSpeedY;
 privatedouble paddlePosX;
 private DateTime PreviousTime;
 privatedouble paddleSpeed;
 private GameState gameState;
 privateint score;
 privateint HiScore;
 privateint Life;
 private Random rnd = new Random();
 public MainPage()
 {
 InitializeComponent();
 HiScore = 0;
 this.textBlockGameStatus.Text = 
 "Please Resize this Window to a pleasant sizen"+
 "Then Click on the game area to Start Game";
 gameState = GameState.GAMEOVER;
 CompositionTarget.Rendering += newFrame;
 }
 privatevoid NewGame()
 {
 score = 0;
 this.textBlockScore.Text = "Score:" + score.ToString();
 this.textBlockHiScore.Text = "Hi Score:" + HiScore.ToString();
 Life = 3;
 NewLife();
 }
 privatevoid NewLife()
 {
 constdouble START_MARGIN = 10;
 Life--;
 this.textBlockLife.Text = Life.ToString() + " Balls Left";
 ballPosX = START_MARGIN + rnd.NextDouble() * 
 (this.GameCanvas.ActualWidth - 2 * START_MARGIN);
 ballSpeedX = 0.3;
 ballPosY = this.GameCanvas.ActualHeight/2 - this.Ball.ActualHeight;
 ballSpeedY = -0.3;
 paddleSpeed = 0;
 paddlePosX = (this.GameCanvas.ActualWidth - this.Paddle.ActualWidth)/2;
 PreviousTime = DateTime.Now;
 this.Paddle.Visibility = System.Windows.Visibility.Visible;
 this.Ball.Visibility = System.Windows.Visibility.Visible;
 gameState = GameState.PAUSED;
 this.textBlockGameStatus.Text = "Ready";
 }
 protectedvoid newFrame(object sender, EventArgs e)
 {
 DateTime now;
 double ellapsedms;
 now = DateTime.Now;
 ellapsedms = (now - PreviousTime).Milliseconds;
 //this.textBlockHiScore.Text = // (1000/ellapsedms).ToString() +" fps"; PreviousTime = now;
 if (gameState == GameState.RUNNING)
 {
 if ((ballSpeedX >0 && ballPosX + 
 Ball.ActualWidth >this.GameCanvas.ActualWidth)
 || (ballSpeedX <0 && ballPosX <1))
 {
 ballSpeedX = -ballSpeedX;
 this.BeepX.Position = TimeSpan.Zero;
 this.BeepX.Play();
 }
 ballPosX += ballSpeedX * ellapsedms;
 if (ballSpeedY >0 
 && ballPosY + Ball.ActualHeight >this.GameCanvas.ActualHeight - this.Paddle.ActualHeight)
 {
 if (ballPosX + this.Ball.ActualWidth > paddlePosX 
 && ballPosX < paddlePosX + this.Paddle.ActualWidth)
 {
 this.BeepPaddle.Position = TimeSpan.Zero;
 this.BeepPaddle.Play();
 ballSpeedY = -ballSpeedY - 0.05;
 ballSpeedX += paddleSpeed;
 score++;
 this.textBlockScore.Text = "Score:" + score.ToString();
 }
 else {
 gameState = GameState.GAMEOVER;
 this.Paddle.Visibility = System.Windows.Visibility.Collapsed;
 this.Ball.Visibility = System.Windows.Visibility.Collapsed;
 if (Life >0)
 {
 this.BeepLost.Position = TimeSpan.Zero;
 this.BeepLost.Play();
 this.textBlockGameStatus.Text = "Click or Press Space";
 }
 else {
 this.BeepGameOver.Position = TimeSpan.Zero;
 this.BeepGameOver.Play();
 if (score > HiScore)
 {
 HiScore = score;
 this.textBlockHiScore.Text = "Hi Score:" + HiScore.ToString();
 }
 this.textBlockGameStatus.Text = 
 "Game OvernClick or Press Space To Start a New Game";
 }
 }
 }
 if (ballSpeedY <0 && ballPosY <1)
 {
 ballSpeedY = -ballSpeedY;
 this.BeepTop.Position = TimeSpan.Zero;
 this.BeepTop.Play();
 }
 ballPosY += ballSpeedY * ellapsedms;
 // move the paddleif ((paddleSpeed >0 && paddlePosX + 
 this.Paddle.ActualWidth <this.GameCanvas.ActualWidth)
 || (paddleSpeed <0 && paddlePosX >1))
 { paddlePosX += paddleSpeed * ellapsedms; }
 }
 //display the sprites at their correct positionthis.Ball.SetValue(Canvas.TopProperty, ballPosY);
 this.Ball.SetValue(Canvas.LeftProperty, ballPosX);
 this.Paddle.SetValue(Canvas.TopProperty,
 this.GameCanvas.ActualHeight - this.Paddle.ActualHeight);
 this.Paddle.SetValue(Canvas.LeftProperty, paddlePosX);
 }
 privatevoid LayoutRoot_KeyDown(object sender, KeyEventArgs e)
 {
 if (gameState == GameState.PAUSED && 
 (e.Key == Key.Left || e.Key == Key.Right))
 {
 gameState = GameState.RUNNING;
 this.textBlockGameStatus.Text = "";
 }
 if (e.Key == Key.Left) { paddleSpeed = -0.5; }
 if (e.Key == Key.Right) { paddleSpeed = +0.5; }
 }
 privatevoid LayoutRoot_KeyUp(object sender, KeyEventArgs e)
 {
 if (e.Key == Key.Left || e.Key == Key.Right) { paddleSpeed = 0; }
 }
 privatevoid button1_LostFocus(object sender, RoutedEventArgs e)
 {
 if (gameState == GameState.RUNNING)
 {
 gameState = GameState.PAUSED;
 this.textBlockGameStatus.Text = "Please Click on The Game";
 }
 }
 privatevoid button1_GotFocus(object sender, RoutedEventArgs e)
 {
 if (gameState == GameState.PAUSED)
 { this.textBlockGameStatus.Text = "Game Paused"; }
 }
 privatevoid button1_Click(object sender, RoutedEventArgs e)
 {
 if (gameState == GameState.GAMEOVER)
 {
 if (Life >0)
 { NewLife(); }
 else { NewGame(); }
 }
 }
 }
}

Points of Interest

游戏循环

对于游戏动画,必须不断重绘播放区域,以更新每个移动项目( 被称为 sprite )的位置。 要平滑移动,重绘必须足够快,通常每秒 50或者 60次。 每个新生成的图像被称为帧,所以我们必须每秒生成 50或者 60帧。

Silverlight 3提供了一种触发重复帧生成的好方法:

CompositionTarget.Rendering+= newFrame 

这确保 newFrame 函数每秒被调用 60次。

function newFrame (you may give any name you want to it) 必须具有以下定义:

void newFrame(object sender, EventArgs e)

游戏布局

最初的想法是为完整的应用程序使用 Canvas,并在它的上绘制用于sprite的图形。 但这是不够的: 我们需要一个项目来接收焦点和检测键盘输入,而 CanvasShape 都不合适。 这就是为什么我添加了一个按钮。 理想情况下,这个按钮应该覆盖整个游戏区域,如果用户点击游戏区域,他将焦点。

由于 Canvas 不调整它的所包含的子级,因这里我被迫添加一个网格并将画布和( 完全透明) 按钮放在游戏区域。 按钮和画布被配置为在整个网格单元上伸展。

我在游戏区域中以( 像"为中心显示消息,我在和透明按钮之间添加了一个 TextBlock 。 我也使用 TextBlock 来显示分数和生命。 但因为它们不是游戏区域的一部分我把它们放在另一个格子上。

最后,我们得到了以下结构:

 layout.png

游戏状态机

状态机与游戏相关联。 当前状态由变量 gameState 跟踪。 可能的状态包括:

  • GameState.GAMEOVER: 这里状态表示屏幕上没有球可以见,并且需要调用 NewGame() 或者 NewLife() 来指定新的起始位置。 当球misses时就达到这个状态。 这也是游戏的初始状态。
  • GameState.PAUSED: 这个状态表明球的位置正确并且是可见的,但是所有的sprite动画都被阻止。 当球位置( 例如,在调用 NewLife() 之后) 初始设置或者游戏失去焦点后发生这里状态。 当球员不再移动球,因为另一个窗口已经经有了焦点,这将会令人烦恼。
  • GameState.RUNNING: 这是动画在屏幕上移动精灵的状态。 当玩家按左或者右箭头键开始( 或者重新启动) 游戏时,就会出现这种状态。

 statemachine.png

键盘和鼠标处理

键盘和鼠标处理依赖于五个事件处理程序:

  • LayoutRoot_KeyDown(object sender, KeyEventArgs e) 当用户按键( 它从按钮上冒泡) 时发生此事件;如果键是左箭头或者右箭头,则将桨速度( 。例如,变量 paddleSpeed ) 设置为正值或者负值取决于所选键。 这个事件处理程序还负责从状态 PAUSED 到状态 RUNNING的转换。
  • LayoutRoot_KeyUp(object sender, KeyEventArgs e) :当播放机释放密钥时发生此事件。 它可以通过将变量 paddleSpeed 设置为零来停止桨的运动。
  • button1_LostFocus(object sender, RoutedEventArgs e) :此事件处理程序检测到覆盖完整游戏区域的按钮的焦点丢失。 这个按钮是这个应用程序中唯一能获得焦点的元素,失去这个焦点意味着应用程序无法处理键盘,并且状态必须变得不一样。
  • button1_GotFocus(object sender, RoutedEventArgs e) :此事件处理程序检测什么时候返回焦点。 这不会改变游戏状态( 需要一个按下事件的按键),所以它只用于更新游戏区域中心的消息。
  • button1_Click(object sender, RoutedEventArgs e) :最后一个事件处理程序检测到覆盖完整游戏区域的透明按钮( 因为它总是有焦点,或者按钮默认键): 空格键) 。这里处理程序用于检测用户请求启动新球的时间。

帧生成和Sprite移动

函数 newFrame 每秒被游戏循环调用 60次。 它必须执行以下操作:

  • 确定自上次调用 newFrame 以来经过的时间。
  • 计算每个sprite的新位置( 在当前游戏中,我们只有两个子画面: 球和球,但在比空间入侵者更复杂的游戏中,你可以以有许多精灵。
  • 确定碰撞并根据需要更新得分和游戏状态。
  • 在正确的位置显示子画面。
  • 1.确定经过的时间

在早期,CPU在每个PC上运行了 4.77兆字节,那时编写的游戏期望 CPU clockspeed 。 然而,当CPU速度升高到 12MHz ( 或者更多)的时候,这些游戏就变得不可以能了,这意味着所有的。 为了避免这个问题,我们需要精确了解自上次调用 newFrame 以来所需的时间。 我们将最后一次调用的时间戳保存在变量 private DateTime PreviousTime; 中,并使用下面的代码来度量经过的时间:

protectedvoid newFrame(object sender, EventArgs e)
{
 DateTime now;
 double ellapsedms;
 now = DateTime.Now;
 ellapsedms = (now - PreviousTime).Milliseconds;
 PreviousTime = now;
. . .
2.计算新的Sprite位置

sprite与 4个变量关联: X 位置,Y 位置,X 速度和Y 速度。

( 注意:球体没有Y 位置,没有Y 速度,因为它总是位于屏幕的底部。)

privatedouble ballPosX;privatedouble ballSpeedX;privatedouble ballPosY;privatedouble ballSpeedY;privatedouble paddlePosX;privatedouble paddleSpeed;

若要移动子画面,我们将速度增加到所用的时间与位置的乘以:

ballPosX += ballSpeedX * ellapsedms;
ballPosY += ballSpeedY * ellapsedms;
paddlePosX += paddleSpeed * ellapsedms;
3 。确定冲突并根据需要更新得分和游戏状态

如果球与墙或者与球相碰,我们就会反转速度。

以下示例显示了与侧墙碰撞的情况:

if ((ballSpeedX >0 && ballPosX + Ball.ActualWidth > 
 this.GameCanvas.ActualWidth) || (ballSpeedX <0 && ballPosX <1))
{
 ballSpeedX = -ballSpeedX;
 this.BeepX.Position = TimeSpan.Zero;
 this.BeepX.Play();
}

注意与墙上球碰撞有关的声音产生。

newFrame 函数中的分数和游戏状态更新时,球在游戏区域底部向下进行更新:

if (ballSpeedY >0 && ballPosY + Ball.ActualHeight > 
 this.GameCanvas.ActualHeight - this.Paddle.ActualHeight)
{
...

如果球击中了球拍:

if (ballPosX + this.Ball.ActualWidth > paddlePosX 
 && ballPosX < paddlePosX + this.Paddle.ActualWidth)

我们演奏声音,改变球的速度,并增加核心。

this.BeepPaddle.Position = TimeSpan.Zero;this.BeepPaddle.Play();
ballSpeedY = -ballSpeedY - 0.05;
ballSpeedX += paddleSpeed;
score++;this.textBlockScore.Text = "Score:" + score.ToString();

否则,弹球丢失了球,所以我们去" GAMEOVER"状态,隐藏球和球球:

else{
 gameState = GameState.GAMEOVER;
 this.Paddle.Visibility = System.Windows.Visibility.Collapsed;
 this.Ball.Visibility = System.Windows.Visibility.Collapsed;
. . .
4.在它的正确位置显示子画面

我们将每个形状的属性 TopLeft 设置为正确的( 新建计算) 值,以便在画布中设置sprite位置。

this.Ball.SetValue(Canvas.TopProperty, ballPosY);this.Ball.SetValue(Canvas.LeftProperty, ballPosX);this.Paddle.SetValue(Canvas.TopProperty, 
 this.GameCanvas.ActualHeight - this.Paddle.ActualHeight);this.Paddle.SetValue(Canvas.LeftProperty, paddlePosX);

声音

Silverlight 3提供了一种交互式地播放MP3声音的方法。

  • 。mp3文件作为你的Silverlight应用程序的资源。
  • 在XAML中为每个的mp3文件声明一个 MediaElement
<MediaElementx:Name="BeepGameOver"Source="GameOver.mp3"AutoPlay="False"/>
  • 当你需要播放声音时,请使用下面的代码:
this.BeepGameOver.Position = TimeSpan.Zero;this.BeepGameOver.Play();

当球击到靠近角落的墙上时,球就可以能会撞到侧壁和顶壁几乎( 但不完全是) 。 也就是说,我们必须在侧墙的端面前为顶墙生成"嘟嘟声"。 如果同时使用同一 MediaElement,第一个"嘟嘟声"将被第二个中断。 这不是我们想要的。 在XAML中,为了避免playing的出现,我们在XAML中创建两个 MediaElement,一个用于侧边墙,另一个用于顶部墙。

<MediaElementx:Name="BeepX"Source="beep1.mp3"AutoPlay="False"/><MediaElementx:Name="BeepTop"Source="beep1.mp3"AutoPlay="False"/>

历史记录

  • Aug 31 2011: 第一个版本。


文章标签:Silverlight  PONG  

Copyright © 2011 HelpLib All rights reserved.    知识分享协议 京ICP备05059198号-3  |  如果智培  |  酷兔英语