目录
乒乓球是一款2人游戏,其中2名玩家同时移动球拍来回击球。玩家1使用键“W”、“A”、“S”和“D”分别表示向上、向下、向左和向右,玩家2使用箭头键。两名球员都试图将球打到对方一侧。如果球越过球员的屏幕一侧,他们的对手就会获得一分。
新更新:添加了屏幕抖动
快速演示:点击这里
源代码:点击这里
描述
Pongs是一款在一周内开发的游戏,您可以在其中使用W键向上移动,A、S、D(同样)和箭头键将球击到另一侧。
图1 - 正在玩的游戏的GIF
如果球越过你的屏幕一侧,对方就会得到一分。游戏会永远进行,并跟踪你的分数。
图2 - 游戏中的路线页面
设置
这个游戏中有一个设置页面。设置是...
图3 - 游戏中的设置页面
- 球速——改变球移动的速度
- 球的大小——改变球的大小
- 桨速——更改桨移动的速度
- 桨的大小——更改桨的大小
- 获胜回合数——玩家赢得比赛所需的获胜次数
球/球拍/墙壁/背景颜色——游戏中可以改变颜色的不同形状。在设置页面上更改颜色会实时更改看板的颜色:
当您更改游戏颜色时,设置页面上的颜色选择器也会更改颜色。
这些特定的颜色让游戏看起来是这样的:
还有暂停和重启按钮,它们可以暂停游戏直到再次点击,或者分别重新启动游戏
图4 - 游戏中的暂停和重启按钮
代码说明
绘制板
我的代码的一个关键部分是在棋盘上绘制所有内容,例如球拍和球。这是通过3个不同的功能完成的。
public void DrawPaddle(Rectangle Paddle, double x, double y)
{
PaddleColor = new SolidColorBrush(SliderInfo.PaddleColor);
Paddle.Width = 1.5 * SliderInfo.PaddleSize;
Paddle.Height = 7.8 * SliderInfo.PaddleSize;
[code deleted for brevity]
Canvas.SetTop(Paddle, y);
Canvas.SetLeft(Paddle, x);
}
上面的代码将球拍画到板上。它是与其他形状分开绘制的,因为它是唯一通过玩家输入移动的形状,并且还需要输入,例如哪个桨以及它的x和y坐标。
public void ReDraw()
{
[code deleted for brevity]
if (WindowState == WindowState.Maximized)
{
Window.Height = (int)System.Windows.SystemParameters.PrimaryScreenHeight;
Window.Width = (int)System.Windows.SystemParameters.PrimaryScreenWidth;
}
Board.Width = Window.Width;
Board.Height = Window.Height;
sbkGameEngine.y1 = Board.Height / 2 - (paddle1.Height / 2);
sbkGameEngine.y2 = sbkGameEngine.y1;
sbkGameEngine.x2 = Board.Width - 32;
sbkGameEngine.x1 = 0;
Ball.Width = 1.5 * SliderInfo.BallSize;
Ball.Height = 1.5 * SliderInfo.BallSize;
[code deleted for brevity]
Canvas.SetTop(Ball, Board.Height / 2 - Ball.Height / 2);
Canvas.SetLeft(Ball, Board.Width / 2 - Ball.Width / 2);
Menu.Width = Board.Width;
if (Board.Width - (SettingsMenu.Width + About.Width + Help.Width) - (1.5 * RestartButton.Width + PauseButton.Width) - 2 > 0)
{
Spacer.Width = Board.Width - (SettingsMenu.Width + About.Width + Help.Width) - (1.5 * RestartButton.Width + PauseButton.Width) - 2;
}
P1Scoreboard.Text = "" + sbkGameEngine.P1Score;
P2Scoreboard.Text = "" + sbkGameEngine.P2Score;
sbkGameEngine.CanBallMove = false;
ReDrawUnmoving();
}
此函数绘制所有不动的形状,以及菜单和板。这样做是为了初始设置电路板,并在必要时重置电路板。
private void ReDrawUnmoving()
{
Boundary.Stroke = WallColor;
[code deleted for brevity]
Boundary.Y1 = 0;
Boundary.Y2 = Board.Height;
BottomWall.Width = Board.Width;
BottomWall.Height = 24;
[code deleted for brevity]
Canvas.SetTop(BottomWall, Board.Height - 63);
Canvas.SetLeft(BottomWall, 0);
Menu.BorderBrush = WallColor;
Spacer.BorderBrush = WallColor;
Menu.Background = WallColor;
Spacer.Background = WallColor;
Board.Background = BackgroundColor;
}
此函数执行与ReDraw()相同的操作,只是这次绘制了不动的部分。
这些功能允许绘制棋盘,这是我游戏的关键元素。这是因为知道棋盘上的所有东西的位置对于玩家以正确的方式做出反应是必要的。
移动球
我比赛的另一个关键部分是球的移动。这是因为游戏的重点是试图阻止球进入你的场地一侧。
public void BallMovement()
{
if (sbkGameEngine.CanBallMove && sbkGameEngine.GamePlayable)
{
Canvas.SetTop(Ball, Canvas.GetTop(Ball) + sbkGameEngine.VMovement);
Canvas.SetLeft(Ball, Canvas.GetLeft(Ball) + sbkGameEngine.HMovement);
}
if (sbkGameEngine.P1Wins)
{
WhoWon_.Text = "Player 1 Wins!";
WhoWon_.Visibility = Visibility.Visible;
RestartText.Visibility = Visibility.Visible;
OnPause(Ball, a);
sbkGameEngine.i = 2;
}
if (sbkGameEngine.P2Wins)
{
WhoWon_.Text = "Player 2 Wins!";
WhoWon_.Visibility = Visibility.Visible;
RestartText.Visibility = Visibility.Visible;
OnPause(Ball, a);
sbkGameEngine.i = 2;
}
}
此函数仅从sbkGameEngine类中获取变量来移动球。它本身不做任何计算,只是通过sbkGameEngine更改的变量来执行引擎告诉它做的事情。
public void BallMovement()
{
log.Info("BallMovement Start");
if (GamePlayable)
{
int P1Top = (int)Canvas.GetTop(mGuiReference.paddle1);
int P1Bottom = (int)(Canvas.GetTop(mGuiReference.paddle1) + mGuiReference.paddle1.Height);
int P1Left = (int)Canvas.GetLeft(mGuiReference.paddle1);
int P1Right = (int)(Canvas.GetLeft(mGuiReference.paddle1) + mGuiReference.paddle1.Width);
int P2Top = (int)Canvas.GetTop(mGuiReference.paddle2);
int P2Bottom = (int)(Canvas.GetTop(mGuiReference.paddle2) + mGuiReference.paddle2.Height);
int P2Left = (int)Canvas.GetLeft(mGuiReference.paddle2);
int P2Right = (int)(Canvas.GetLeft(mGuiReference.paddle2) + mGuiReference.paddle2.Width);
int BallTop = (int)Canvas.GetTop(mGuiReference.Ball);
int BallBottom = (int)(Canvas.GetTop(mGuiReference.Ball) + mGuiReference.Ball.Height);
int BallLeft = (int)Canvas.GetLeft(mGuiReference.Ball);
int BallRight = (int)(Canvas.GetLeft(mGuiReference.Ball) + mGuiReference.Ball.Width);
if ((P2Bottom > BallTop && P2Top < BallBottom && BallLeft < P2Right && BallRight > P2Left && HMovement == 1) || (P1Bottom > BallTop && P1Top < BallBottom && BallLeft < P1Right && BallRight > P1Left && HMovement == -1))
{
HMovement *= -1;
Console.Beep(37, 10);
}
if (BoundaryCheck(mGuiReference.Ball, 25, (int)(mGuiReference.Board.Height - (mGuiReference.BottomWall.Height * 2) - 5), 0, 0, true, true, false, false) == false)
{
VMovement *= -1;
Console.Beep(70, 5);
}
if (Canvas.GetLeft(mGuiReference.Ball) >= mGuiReference.Board.Width)
{
P1Score++;
mGuiReference.ReDraw();
log.Info("Player 1 scored!");
if (P1Score == SliderInfo.RoundsToWin)
{
P1Wins = true;
}
}
if (Canvas.GetLeft(mGuiReference.Ball) + 15 <= 0)
{
P2Score++;
mGuiReference.ReDraw();
log.Info("Player 2 scored!");
if (P2Score == SliderInfo.RoundsToWin)
{
P1Wins = true;
}
}
}
log.Info("BallMovement End");
}
此函数是实际执行计算的函数。当满足某些条件时,它会改变控制球移动方向的变量,例如在击球后交换方向。
这两种功能结合在一起,可以移动球,并允许球与其环境相互作用。
球拍运动
创建游戏所需的另一个元素是球拍的运动,因为它是唯一由玩家控制的形状。
public void OnKeyDown(object sender, KeyEventArgs e)
{
if (AllowedKeys.Contains(e.Key))
{
if (i == 0)
{
KeysPressed.Add(e.Key);
CanBallMove = true;
}
}
}
此函数在按下某个键时,会将该键添加到哈希集中。该哈希集稍后将用于识别哪些键被按下,哪些键未被按下。
public void OnKeyUp(object sender, KeyEventArgs e)
{
if (KeysPressed.Contains(e.Key))
{
KeysPressed.Remove(e.Key);
}
}
此功能删除松开键并在按下某个键时运行。
public void PressedKeys(/*object? sender, EventArgs e*/)
{
if (GamePlayable)
{
if (KeysPressed.Contains(Key.Up) && BoundaryCheck(mGuiReference.paddle2, 25, (int)mGuiReference.Board.Height - 78, (int)mGuiReference.Board.Width / 2, (int)mGuiReference.Board.Width, true, false, false, false) == true)
{
y2 -= 2;
}
if (KeysPressed.Contains(Key.W) && BoundaryCheck(mGuiReference.paddle1, 25, (int)mGuiReference.Board.Height - 78, (int)mGuiReference.Board.Width / 2, (int)mGuiReference.Board.Width, true, false, false, false) == true)
{
y1 -= 2;
}
if (KeysPressed.Contains(Key.Down) && BoundaryCheck(mGuiReference.paddle2, 25, (int)mGuiReference.Board.Height - 78, (int)mGuiReference.Board.Width / 2, (int)mGuiReference.Board.Width, false, true, false, false) == true)
{
y2 += 2;
}
if (KeysPressed.Contains(Key.S) && BoundaryCheck(mGuiReference.paddle1, 25, (int)mGuiReference.Board.Height - 78, (int)mGuiReference.Board.Width / 2, (int)mGuiReference.Board.Width, false, true, false, false) == true)
{
y1 += 2;
}
if (KeysPressed.Contains(Key.Left) && BoundaryCheck(mGuiReference.paddle2, 25, (int)mGuiReference.Board.Height - 78, (int)mGuiReference.Board.Width / 2, (int)mGuiReference.Board.Width / 2, false, false, true, false) == true)
{
x2 -= 2;
}
if (KeysPressed.Contains(Key.A) && BoundaryCheck(mGuiReference.paddle1, 25, (int)mGuiReference.Board.Height - 78, 0, (int)mGuiReference.Board.Width / 2, false, false, true, false) == true)
{
x1 -= 2;
}
if (KeysPressed.Contains(Key.Right) && BoundaryCheck(mGuiReference.paddle2, 25, (int)mGuiReference.Board.Height - 78, 0, (int)mGuiReference.Board.Width - 20, false, false, false, true) == true)
{
x2 += 2;
}
if (KeysPressed.Contains(Key.D) && BoundaryCheck(mGuiReference.paddle1, 25, (int)mGuiReference.Board.Height - 78, 0, (int)mGuiReference.Board.Width / 2, false, false, false, true) == true)
{
x1 += 2;
}
}
}
上述函数由不同的函数重复运行,并检查哈希集中是否有任何键。如果一个键在哈希集中,它将执行相应的移动,例如当向上键在哈希集中时,将玩家2向上移动。
这些都结合在一起,使玩家的按键输入能够与游戏中球拍采取的动作相对应。
“设置”页面
设置页面需要滑块在滑块更改时更改变量,以便滑块所说的任何事情都会发生。这是使用XAML和数据绑定完成的。
public static double BallSpeed { get; set; }
public static double BallSize { get; set; }
public static Color PaddleColor { get; set; }
public static Color WallColor { get; set; }
滑块信息类包含滑块要对其执行操作的一些变量。这些变量将在滑块移动时发生变化。
<Slider x:Name="BallSpeedSlider" Margin="136,72,0,0" Maximum="9" Minimum="2" IsSnapToTickEnabled="True" Value="{Binding BallSpeed, Mode=TwoWay}" ValueChanged="OnValueChanged" HorizontalAlignment="Left" Width="226" Height="123" VerticalAlignment="Top" Grid.ColumnSpan="2"/>
<xctk:ColorPicker x:Name="Wall_Color_Picker" Margin="207,339,0,0" Height="23" VerticalAlignment="Top" HorizontalAlignment="Left" Width="101" ShowDropDownButton = "False" ShowTabHeaders="False" ColorMode="ColorCanvas" SelectedColor="{Binding WallColor, Mode=TwoWay}" Grid.Column="1"/>
以上是在XAML中制作的滑块和颜色选取器的示例,这些滑块和颜色选取器使用数据绑定更改变量。数据绑定将滑块的值绑定到变量,将绑定的波特设置为TwoWay允许在更改滑块时,变量也会更改,反之亦然。
这些变量与球的速度或球拍的颜色等属性相关联,因此当滑块或颜色选择器发生变化时,属性也会发生变化。这允许设置页面以简单的方式运行。
数据绑定
SelectedColor="{Binding WallColor, Mode=TwoWay}"
以上是XAML中的颜色绑定示例。SelectedColor是在颜色选取器上选择的颜色。您可以看到它绑定到WallColor,因为在大括号中它说Binding WallColor。WallColor可以替换为任何其他变量,并且颜色选取器将改为绑定到该变量,前提是该变量接受颜色输入。模式是双向的,允许更改颜色选择器以更改变量,并更改变量以更改颜色选择器。有4种类型的数据绑定。
1. OneWay——只有源属性(如颜色选取器或滑块)中的更改才会影响变量
2. TwoWay——变量中的更改会影响source属性,而source属性中的更改会影响变量
3. OneWayToSource——只有变量中的更改才会影响source属性
4. OneTime——使用变量初始化应用程序时仅更新一次源属性
这4种类型都有自己的用例,但我只在我的项目中使用了TwoWay,因为我的游戏的任何功能都不需要其他类型。
摇晃屏幕
通过在玩家得分时摇晃屏幕,它可以为玩家创造视觉反馈,让他们感受到更高的成就感。虽然一开始看起来很容易编码,但它有几个障碍。
为了创造这种效果,我使用了故事板,因为这是一种让动画播放的简单方法。故事板看起来像这样。
<Storyboard RepeatBehavior="0:0:1" Name="DownRight">
<DoubleAnimation Storyboard.TargetName="Window"
Storyboard.TargetProperty="(Window.Left)" From="{Binding WindowLeft, Mode=TwoWay}" To="{Binding Path=WindowLeftTo, Mode=TwoWay}"
Duration="0:0:0:0.03" BeginTime="0:0:0" AutoReverse="true" RepeatBehavior="3x" FillBehavior="Stop"/>
<DoubleAnimation Storyboard.TargetName="Window"
Storyboard.TargetProperty="(Window.Top)" From="{Binding WindowTop, Mode=TwoWay}" To="{Binding Path=WindowTopTo, Mode=TwoWay}"
Duration="0:0:0:0.03" BeginTime="0:0:0" AutoReverse="true" RepeatBehavior="3x" FillBehavior="Stop"/>
</Storyboard>
此故事板使用两个双动画上下以及从左到右移动窗口。双动画需要输入From(窗口应开始动画的位置)和To(窗口应结束动画的位置)。为此,我使用了窗口的左侧和顶部值,对于to,我使用了一个变量,该变量是窗口的左侧+10和窗口的顶部+10。
若要获取这些值,您可能认为可以使用“Application.Current.MainWindow.Left”和“Application.Current.MainWindow.Top”。但是,由于WPF的制作方式,如果窗口从未移动,它将返回NaN。这很重要,因为如果窗口从未移动过,这会导致代码在运行动画时崩溃。因此,我不得不使用不同的方法来获取窗口的坐标。
[StructLayout(LayoutKind.Sequential)]
public struct RECT
{
public int Left;
public int Top;
public int Right;
public int Bottom;
}
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
public event PropertyChangedEventHandler PropertyChanged;
此代码创建一个获取窗口坐标的矩形。因此,允许代码找到矩形的坐标,该坐标与窗口的坐标相同。并用于情节提要中的from和to参数。
若要获取窗口的新坐标,当窗口打开或移动时,它会调用以下函数。
private void GetNewLocation()
{
RECT rect;
IntPtr windowHandle = new WindowInteropHelper(Window).Handle;
GetWindowRect(Process.GetCurrentProcess().MainWindowHandle, out rect);
WindowLeft = rect.Left;
WindowTop = rect.Top;
PropertyChanged(this, new PropertyChangedEventArgs(nameof(WindowLeft)));
PropertyChanged(this, new PropertyChangedEventArgs(nameof(WindowLeftTo)));
PropertyChanged(this, new PropertyChangedEventArgs(nameof(WindowTop)));
PropertyChanged(this, new PropertyChangedEventArgs(nameof(WindowTopTo)));
}
此函数使用窗口的新属性创建矩形,然后更改与更改对应的XAML中使用的变量的值。最终结果如下所示。
气球
一旦游戏结束,气球就会从地上飞起来。为了达到这个效果,使用了几种新方法。
Thread CreateBalloons = new Thread(BalloonRunner);
CreateBalloons.Start();
public void BalloonRunner()
{
sbkGameEngine.BalloonRun = true;
while (sbkGameEngine.BalloonRun)
{
if (Balloons)
{
Dispatcher.Invoke(() =>
CreateBalloon()
);
}
Dispatcher.Invoke(() =>
MoveBalloons()
);
Thread.SpinWait(1000000);
}
}
这个线程调用BalloonRunner方法,该方法重复调用CreateBalloon和MoveBalloon。然而,它只调用CreateBalloon,如果气球布尔值被设置为true,这只在游戏结束时完成。
Interval -= 10;
if (Interval < 1)
{
...
Interval = RandomInt.Next(90, 150);
}
上面的代码使用int Interval只在每几个调用时运行代码。这允许气球不会很快填满屏幕,并占用他们的时间。
if (NumberOfBalloons == 99)
{
Balloons = false;
}
这段代码检查变量NumberOfBalloons是否超过99,如果超过99,将通过禁用运行程序来结束线程。每次创建气球时,NumberOfBalloons增加1。
int BalloonColor = RandomInt.Next(1, 6);
switch (BalloonColor)
{
case 1:
BalloonImage.ImageSource = new BitmapImage(new Uri("pack://application:,,,/PongsSBK;component/pictures/RedBalloon.png"));
break;
case 2:
BalloonImage.ImageSource = new BitmapImage(new Uri("pack://application:,,,/PongsSBK;component/pictures/OrangeBalloon.png"));
break;
case 3:
BalloonImage.ImageSource = new BitmapImage(new Uri("pack://application:,,,/PongsSBK;component/pictures/YellowBalloon.png"));
break;
case 4:
BalloonImage.ImageSource = new BitmapImage(new Uri("pack://application:,,,/PongsSBK;component/pictures/GreenBalloon.png"));
break;
case 5:
BalloonImage.ImageSource = new BitmapImage(new Uri("pack://application:,,,/PongsSBK;component/pictures/BlueBalloon.png"));
break;
}
代码使用switch函数来替换5个if块。RandomInt从1-5中创建一个随机整数,然后switch函数检查它得到了哪个数字,并将相应的气球颜色添加到画笔BalloonColor中。
Rectangle NewBalloon = new Rectangle
{
Tag = "Balloon",
Width = 37,
Height = 47,
Fill = BalloonImage
};
Canvas.SetTop(NewBalloon, (int)((System.Windows.Controls.Panel)Application.Current.MainWindow.Content).ActualHeight);
Canvas.SetLeft(NewBalloon, RandomInt.Next(0, (int)((System.Windows.Controls.Panel)Application.Current.MainWindow.Content).ActualWidth) - (NewBalloon.Width / 2));
Board.Children.Add(NewBalloon);
这段代码使用之前的气球笔刷填充创建了一个矩形。这允许矩形显示气球的图像,从而创建一个气球。
private void MoveBalloons()
{
foreach (var x in Board.Children.OfType<Rectangle>())
{
if ((string)x.Tag == "Balloon")
{
Canvas.SetTop(x, Canvas.GetTop(x) - speed);
Canvas.SetLeft(x, Canvas.GetLeft(x) - (1 * RandomInt.Next(-1, 2)));
}
if (Canvas.GetTop(x) < 0 - x.Height)
{
itemRemover.Add(x);
}
}
foreach (Rectangle x in itemRemover)
{
Board.Children.Remove(x);
}
}
MoveBalloons方法使用foreach函数来移动每个气球。它首先检查每个矩形是否有标签“balloon”(只有气球才有),然后将其随机向上移动到左边。一旦气球超过了一定的高度,它就被添加到列表itemRemover中。下一个foreach函数检查每个矩形是否在窗口的顶部,如果在则删除。
当游戏完成时,所有这些都将创造出能够飞起来的气球。当玩家获得一定数量的分数(默认为5分)时,游戏结束。
运行应用程序
- 转到GitHub链接:单击此处
- 转到标有“快速演示”的文件夹
- 下载“ZippedDemo.zip”
- 解压缩文件
- 运行Pongs.exe
https://www.codeproject.com/Articles/5382198/Pongs-A-Simple-Pong-Like-Game