C#斯诺克(译文)
By robot-v1.0
本文链接 https://www.kyfws.com/games/c-snooker-zh/
版权声明 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
- 12 分钟阅读 - 5963 个词 阅读量 0C#斯诺克(译文)
原文地址:https://www.codeproject.com/Articles/45346/C-Snooker
原文作者:Marcelo Ricardo de Oliveira
译文由本站 robot-v1.0 翻译
前言
Sound-enabled pool game for C#.
C#的启用声音的台球游戏.
介绍(Introduction)
本文旨在分享一些用C#编写台球游戏的不错发现.尽管我的首要动机是为读者提供一些有用的编程信息,但我也希望您真的喜欢游戏本身.(This article is intended to share some nice discoveries of writing a pool game in C#. Although my first motivation is to give readers some useful programming information, I’m also hoping you really enjoy the game itself.)
背景(Background)
游戏建立在三个基石之上:(The game is built over three cornerstones:)
- 碰撞检测/碰撞解决(Collision detection/collision resolution):首先,撞球游戏必须具有碰撞检测并正确处理,这一点至关重要.当球移动时,必须始终将它们限制在边界之内,并且除非落入口袋,否则必须保留在游泳池中.当一个球与其他球或边界碰撞时,您必须知道它,并采取措施解决碰撞,然后再将碰撞球放在屏幕上.碰撞本身并没有什么问题(例如,您可以测试两个球是否彼此靠近而不是其半径的两倍).真正的问题是,如果碰撞球是真实物体,则要确定此时碰撞球应位于何处,并计算其最终方向.我很难尝试正确解决碰撞,在我自己放弃重新发明轮子之后,我终于求助于Google.尽管有许多文章介绍了解决冲突的方法,但我最终还是使用了这一简单明了的方法(: First of all, it’s essential for a pool game to have a collision detection and handle it properly. When the balls are moving, they must always be confined inside the borders, and remain on the pool unless they fall into the pockets. When a ball collides with other balls or borders, you must know it, and take action to resolve the collision prior to placing the colliding ball on the screen. The collision itself is not that problematic (for example, you could just test whether two balls are closer to each other than twice their radius). The real problem is to decide where the colliding balls should be at that moment if they were real objects, and also calculate their resulting directions. I had a hard time trying to resolve collisions properly, and after giving up reinventing the wheel myself, I finally resorted to Google. Although there are many articles explaining collision resolution, I ended up using this simple and straight to the point) 检测与处理(Detection and Handling) Matthew McDole的文章.(article from Matthew McDole.)
- 快速流畅的图形渲染(Fast and smooth graphics rendering):最初,我尝试使用计时器来控制渲染过程.每次滴答时,都会计算出球的位置,然后绘制图形.问题在于,通常每个滴答声的计算时间都不同,因为有时我只移动一个球,而在另一些时候,我有10个球同时碰撞和移动.因此计算工作量有所不同.这种差异影响了渲染,并在某些点上看起来像是"剪切"的.这真令人沮丧.例如,如果您使用其他斯诺克游戏,您会注意到每个镜头都有流畅的渲染.然后,我首先通过进行所有计算来重构代码,然后在内存中创建一个包含帧序列的"电影"(每个帧是池背景上某个时间点球的快照).在所有球停止移动之后,完成"电影"并播放.最初,这似乎需要太多努力,但是对于这样的游戏,渲染速度至关重要.如果将游戏移植到XNA技术上可能会更容易,但是我不想强迫CodeProject用户下载其他Visual Studio软件包.(: At first, I tried to use a timer to control the rendering process. At every tick, the positions of the balls were calculated and then the graphics were rendered. The problem is that, usually, the calculation time was different for each tick, because sometimes I had just one ball moving, while at others, I had 10 balls colliding and moving at the same time. So the calculation effort was different. This difference affected the rendering, and appeared like it was “cut” at some points. This was frustrating. If you take, for example, other snooker games, you’ll notice that each shot has a fluid rendering. Then, I refactored the code by doing all calculations first, and then created a “movie” in memory containing the sequence of frames (each frame is a snapshot of the balls at some point in time, over the pool background). After all balls stopped moving, the “movie” is done and played. At first, this looks like too much effort, but for a game like this, the speed of rendering is critical. It might have been easier if I ported the game to XNA technology, but I didn’t want to force CodeProject users to download additional Visual Studio packages.)
- 逼真的音效(Realistic sound effects):当我终于可以使用图形时,我发现缺少某些东西.我希望游戏能够发出声音,使其更加逼真有趣.经过研究,我发现了一些(: When I finally got the graphics working, I noticed something was missing. I wanted the game to have sounds to make it more realistic and exciting. After some research, I found a few)**.wav(.wav)**这些文件可能对提示击打母球,击球和其他真实台球游戏的声音很有用.然后,我尝试使用默认播放(files that could be useful for the cue hitting the cue ball, the balls hitting each other, and other real pool game sounds. Then, I tried playing it with the default)
System.Media.SoundPlayer
对象,但很快我注意到它不能同时播放声音:乳清播放声音,所有正在执行的声音都停止了.幸运的是,我发现了(object, but soon I noticed it doesn’t play simultaneous sounds: whey you play a sound, all executing sounds are stopped. Fortunately, I found the wonderful) 巴生(IrrKlang) 音频引擎并解决了问题.它具有一个非常有趣的3D音频引擎,您可以在其中定义声音和XYZ坐标.试想一下第一人称射击游戏.您正走在一条黑暗的街道上,并且听到从右侧传来的嘶嘶声.当您继续走路时,声音会变大.再走一点,右耳的声音和左耳的声音一样大.然后,声音从您的右侧发出.最后,您注意到您后面跟随着一个诡诈的怪物,该怪物越来越靠近,从右向左掠过.您可以通过告诉IrrKlang引擎播放"(audio engine and got the problem solved. It has a very interesting 3D audio engine, where you can define the sound and the XYZ coordinates. Just think about a first person shooter game. You are walking by a dark street, and you are hearing a soft roar coming from your right side. As you keep walking, the sound becomes louder. Walking a little more, the sound is as loud at your right ear as at your left ear. Then, the sound comes from your right side. At the end, you notice you have been followed by a treacherous monster, who was getting closer, passing from your right to your left. You can do something similar by telling the IrrKlang engine to play a “)**咆哮(roar.wav)**会以不同的XYZ坐标发出声音,并将参考点视为第一人称射击者(您).在此游戏中,我使用3D音频引擎根据源的坐标播放声音.(” sound in different XYZ coordinates, considering the reference point as being the first person shooter (you). In this game, I used the 3D audio engine to play the sound according to the coordinates of the source.)
游戏(The Game)
规则(Rules)
游戏本身是简化的斯诺克游戏.而不是15个红色球,它只有6个.每个红色球给1分,而"颜色"球给2到7分(黄色=2,绿色=3,棕色=4,蓝色=5,粉红色=6 ,黑色=7).(The game itself is a simplified snooker game. Instead of 15 red balls, it has only 6. Each red ball grants 1 point, while the “color” balls grant from 2 to 7 points (Yellow=2, Green=3, Brown=4, Blue=5, Pink=6, Black=7).) 玩家必须使用母球(白球)来瞄准"球".只要桌上还有红色球," ball on"(红色球)总是在红色球和彩色球之间交替.一旦将所有红色球都装好,上面的球就是价值较低的彩色球.如果球员错过了球,或者击中了除了球以外的其他球,那就是错误.如果球员将球以外的其他球击球,那是错误的.如果球员未能用母球击打任何其他球,那就是故障.玩家只会在没有错误的情况下得分.将故障点授予对手.当所有球都装好(母球除外)后,游戏结束.(The player must use the cue ball (white ball) to aim to pot the “ball on”. The “ball on” is always alternating between a red ball and a color ball, as long as there are still red balls on the table. Once all red balls are potted, the ball on is the less valuable color ball. If the player misses the ball on, or hits another ball other than the ball on, it is a fault. If the player pots a ball other than the ball on, it is a fault. If the player fails to hit any other ball with the cue ball, it is a fault. The player will only score if there are no faults. The fault points are granted to the opponent. The game is over when all balls are potted (except for the cue ball).)
int strokenBallsCount = 0;
foreach (Ball ball in strokenBalls)
{
//causing the cue ball to first hit a ball other than the ball on
if (strokenBallsCount == 0 && ball.Points != currentPlayer.BallOn.Points)
currentPlayer.FoulList.Add((currentPlayer.BallOn.Points < 4 ? 4 :
currentPlayer.BallOn.Points));
strokenBallsCount++;
}
//Foul: causing the cue ball to miss all object balls
if (strokenBallsCount == 0)
currentPlayer.FoulList.Add(4);
foreach (Ball ball in pottedBalls)
{
//causing the cue ball to enter a pocket
if (ball.Points == 0)
currentPlayer.FoulList.Add(4);
//causing a ball not on to enter a pocket
if (ball.Points != currentPlayer.BallOn.Points)
currentPlayer.FoulList.Add(currentPlayer.BallOn.Points < 4 ? 4 :
currentPlayer.BallOn.Points);
}
if (currentPlayer.FoulList.Count == 0)
{
foreach (Ball ball in pottedBalls)
{
//legally potting reds or colors
wonPoints += ball.Points;
}
}
else
{
currentPlayer.FoulList.Sort();
lostPoints = currentPlayer.FoulList[currentPlayer.FoulList.Count - 1];
}
currentPlayer.Points += wonPoints;
otherPlayer.Points += lostPoints;
用户界面(User Interface)
屏幕上有三个重要区域:池,乐谱和提示控制.(There are three important areas on the screen: the pool, the score, and the cue control.)
游泳池(The Pool)
图1.游戏池以黄色显示其许多边界.(Figure 1. Game pool displaying its many borders in yellow.)桌子是桃花心木模型,上面覆盖着精美的蓝色培根.有六个口袋,每个角各有一个,长边中间还有两个.(The table is a mahogany model, covered with fine blue baize. There are six pockets, one for each corner, and two more in the middle of the long sides.) 轮到您时,将鼠标移到桌子上时,鼠标指针会以目标(已经选择了球的位置)或手(当您必须选择球的位置)的形式出现.当您单击鼠标左键时,母球将从其原始点运行到选择点.(When it is your turn, when you move the mouse over the table, the mouse pointer takes the form of a target (when the ball on is already selected) or a hand (when you must select a ball on). When you hit the left mouse button, the cue ball will run from its original point to the select point.)
void HitBall(int x, int y)
{
//Reset the frames and ball positions
ClearSequenceBackGround();
ballPositionList.Clear();
poolState = PoolState.Moving;
picTable.Cursor = Cursors.WaitCursor;
//20 is the maximum velocity
double v = 20 * (currentPlayer.Strength / 100.0);
//Calculates the cue angle, and the translate velocity (normal velocity)
double dx = x - balls[0].X;
double dy = y - balls[0].Y;
double h = (double)(Math.Sqrt(Math.Pow(dx, 2) + Math.Pow(dy, 2)));
double sin = dy / h;
double cos = dx / h;
balls[0].IsBallInPocket = false;
balls[0].TranslateVelocity.X = v * cos;
balls[0].TranslateVelocity.Y = v * sin;
Vector2D normalVelocity = balls[0].TranslateVelocity.Normalize();
//Calculates the top spin/back spin velocity,
//in the same direction as the normal velocity, but in opposite angle
double topBottomVelocityRatio =
balls[0].TranslateVelocity.Lenght() * (targetVector.Y / 100.0);
balls[0].VSpinVelocity = new Vector2D(-1.0d * topBottomVelocityRatio *
normalVelocity.X, -1.0d * topBottomVelocityRatio * normalVelocity.Y);
//xSound defines if the sound is coming from the left or the right
double xSound = (float)(balls[0].Position.X - 300.0) / 300.0;
soundTrackList[snapShotCount] = @"Sounds\Shot01.wav" + "|" + xSound.ToString();
//Calculates the ball positions as long as there are moving balls
while (poolState == PoolState.Moving)
MoveBalls();
currentPlayer.ShotCount++;
}
分数(The Score)
分数是一个老式的木制面板,显示了两个玩家的分数.此外,它还显示球的闪烁图像.(The score is a vintage wooden panel that shows the two players' scores. In addition, it also shows a blinking image of the ball on.)
private void timerBallOn_Tick(object sender, EventArgs e)
{
if (playerState == PlayerState.Aiming || playerState == PlayerState.Calling)
{
picBallOn.Top = 90 + (currentPlayer.Id - 1) * 58;
showBallOn = !showBallOn;
picBallOn.Visible = showBallOn;
}
}
图2.我对着计算机.(Figure 2. Me against the computer.)#### 提示控制(The Cue Control)
提示控制是一块拉丝的钢板,它有两个目标:控制提示强度(上方的红色线)和控制提示球"旋转".您可以使用强度条根据情况进行更精确的拍摄.并且,如果您知道如何进行"顶部旋转"和"反向旋转",则旋转控制很有用. “顶部旋转”,也称为"跟随",可以增加母球的速度并在母球击中另一个球时提供更大的张开角度.另一方面,“向后旋转"会降低母球的速度,并在击中目标球后将母球向后移动.这也会影响击球后的合成角度,通常会使母球弯曲.(The Cue Control is a brushed steel panel, and has two goals: to control the cue strength (the upper red line) and to control the cue ball “spin”. You can use the strength bar to give a more precise shot according to the situation. And, the spin control is useful if you know how to do the “top spin” and the “back spin”. The “top spin”, also known as “follow”, increases the cue ball velocity and gives a more open angle when the cue ball hits another ball. The “back spin”, on the other hand, decreases the cue ball velocity, and moves back the cue ball the way it came after striking the object ball. This also affects the resulting angle after the hit, and usually makes the cue ball to move in a curve.)
注意(Notice):我没有实现"侧旋”,因为我认为这会花费太多的精力,并且只会增加文章的篇幅.(: I didn’t implement the “side spin”, because I thought it would require too much effort and would add little to the article.)
图3.强度控制和旋转控制.(Figure 3. Strength control and spin control.)
图4.旋转路径.(Figure 4. Spin paths.)|
|
|
图5.动作中的不同旋转:正常(无旋转),向后旋转和顶部旋转.(Figure 5. Different spins in action: normal (no spin), back spin, and top spin.)```c# public void ResolveCollision(Ball ball) { // get the mtd Vector2D delta = (position.Subtract(ball.position)); float d = delta.Lenght(); // minimum translation distance to push balls apart after intersecting Vector2D mtd = delta.Multiply((float)(((Ball.Radius + 1.0 + Ball.Radius + 1.0) - d) / d));
// resolve intersection --
// inverse mass quantities
float im1 = 1f;
float im2 = 1f;
// push-pull them apart based off their mass
position = position.Add((mtd.Multiply(im1 / (im1 + im2))));
ball.position = ball.position.Subtract(mtd.Multiply(im2 / (im1 + im2)));
// impact speed
Vector2D v = (this.translateVelocity.Subtract(ball.translateVelocity));
float vn = v.Dot(mtd.Normalize());
// sphere intersecting but moving away from each other already
if (vn > 0.0f)
return;
// collision impulse
float i = Math.Abs((float)((-(1.0f + 0.1) * vn) / (im1 + im2)));
Vector2D impulse = mtd.Multiply(1);
int hitSoundIntensity = (int)(Math.Abs(impulse.X) + Math.Abs(impulse.Y));
if (hitSoundIntensity > 5)
hitSoundIntensity = 5;
if (hitSoundIntensity < 1)
hitSoundIntensity = 1;
double xSound = (float)(ball.Position.X - 300.0) / 300.0;
observer.Hit(string.Format(@"Sounds\Hit{0}.wav",
hitSoundIntensity.ToString("00")) + "|" + xSound.ToString());
// change in momentum
this.translateVelocity = this.translateVelocity.Add(impulse.Multiply(im1));
ball.translateVelocity = ball.translateVelocity.Subtract(impulse.Multiply(im2));
}
### 电影(*The Movie*)

图6.内存中的框架.(*Figure 6. In-memory frames.*)每次拍摄时,都会启动新的"电影".只要桌子上至少有一个运动球,该应用程序就会计算所有运动并列出球的位置.当所有球都静止不动时,球位置列表用于创建内存中的帧,就像电影中的帧一样.创建所有帧后,将以流畅,快速的方式播放影片.(*At every shot, a new "movie" is started. The application calculates all movements and makes a list of ball positions as long as there is at least one moving ball on the table. When all balls are still, the ball positions list is used to create the in-memory frames, just like the frames in a movie. When all frames are created, the movie is played, in a smooth and fast way.*)
```c#
void DrawSnapShots()
{
XmlSerializer serializer =
new XmlSerializer(typeof(List<ballposition>));
string path =
Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
using (StreamWriter sw = new StreamWriter(Path.Combine(path,
@"Out\BallPositionList.xml")))
{
serializer.Serialize(sw, ballPositionList);
}
ClearSequenceBackGround();
int snapShot = -1;
Graphics whiteBitmapGraphics = null;
//For each ball, draws an image of that ball
//over the pool background image
foreach (BallPosition ballPosition in ballPositionList)
{
if (ballPosition.SnapShot != snapShot)
{
snapShot = ballPosition.SnapShot;
whiteBitmapGraphics = whiteBitmapGraphicsList[snapShot];
}
//draws an image of a ball over the pool background image
whiteBitmapGraphics.DrawImage(balls[ballPosition.BallIndex].Image,
new Rectangle((int)(ballPosition.X - Ball.Radius),
(int)(ballPosition.Y - Ball.Radius),
(int)Ball.Radius * 2, (int)Ball.Radius * 2), 0, 0,
(int)Ball.Radius * 2, (int)Ball.Radius * 2, GraphicsUnit.Pixel, attr);
}
}
private void PlaySnapShot()
{
//Plays an individual frame, by replacing the image of the picturebox with
//the stored image of a frame
picTable.Image = whiteBitmapList[currentSnapShot - 1]; ;
picTable.Refresh();
string currentSound = soundTrackList[currentSnapShot - 1];
if (currentSound.Length > 0)
{
currentSound += "|0";
string fileName = currentSound.Split('|')[0];
Decimal x = -1 * Convert.ToDecimal(currentSound.Split('|')[1]);
//Plays the sound considering whether the sounds comes from left or right
soundEngine.Play3D(fileName, 0, 0, (float)x);
}
currentSnapShot++;
}
声音引擎(Sound Engine)
正如我之前提到的,游戏没有使用(As I mentioned previously, the game doesn’t use the) System.Media.SoundPlayer
对象播放声音,因为播放的每个新声音都会"剪切"当前声音.这意味着您不会听到一个球掉进口袋里的声音以及两个球同时碰撞的声音.我用(object to play sounds, because each new sound played “cuts” the current sound. This means, you can’t hear the sound of a ball falling into a pocket and the sound of two balls colliding at the same time. I solved this with the) 巴生(IrrKlang) 零件.另外,我还告诉声音引擎根据声音来源的位置播放声音.例如,如果一个球掉入右上角的口袋,您的右耳会听到声音更大.如果一个球击中了桌子下角的另一个球,您会听到声音从左边传来.我在互联网上发现了一些很酷的斯诺克台球声音,根据碰撞球的速度,其中有些是软声还是硬声:(component. In addition, I also tell the sound engine to play the sound according to the position of the source of the sound. For example, if a ball falls into the upper right pocket, you hear the sound louder at your right ear. If a ball hits another one at the lower corner of the table, you hear the sound coming from the left. There are some cool snooker sounds I found on the internet, and some of them are soft or hard depending on the velocity of the colliding balls:)
图7.声音效果.(Figure 7. Sound effects.)```c# if (currentSound.Length > 0) { currentSound += “|0”; string fileName = currentSound.Split('|')[0]; Decimal x = -1 * Convert.ToDecimal(currentSound.Split('|')[1]);
//Plays the sound considering whether the sounds comes from left or right
soundEngine.Play3D(fileName, 0, 0, (float)x);
}
### 我(*A.I.*)
所谓的"鬼球"在游戏情报中起着重要作用.当计算机轮流玩耍时,系统会指示它寻找所有好的"鬼球",以便它有更多的成功机会.幽灵球是您可以瞄准的靠近"开球"的位置,因此该球应落入特定的口袋中.(*The so called "Ghost balls" play an important role in the game intelligence. When the computer plays in its turn, it is instructed to look for all good "ghost balls", so that it can have more chances of success. Ghost balls are the spots close to the "ball on", that you can aim to, so that the ball should fall into a specific pocket.*)
```c#
private List GetGhostBalls(Ball ballOn)
{
List ghostBalls = new List();
int i = 0;
foreach (Pocket pocket in pockets)
{
//distances between pocket and ball on center
double dxPocketBallOn = pocket.HotSpotX - ballOn.X;
double dyPocketBallOn = pocket.HotSpotY - ballOn.Y;
double hPocketBallOn = Math.Sqrt(dxPocketBallOn *
dxPocketBallOn + dyPocketBallOn * dyPocketBallOn);
double a = dyPocketBallOn / dxPocketBallOn;
//distances between ball on center and ghost ball center
double hBallOnGhost = (Ball.Radius - 1.0) * 2.0;
double dxBallOnGhost = hBallOnGhost * (dxPocketBallOn / hPocketBallOn);
double dyBallOnGhost = hBallOnGhost * (dyPocketBallOn / hPocketBallOn);
//ghost ball coordinates
double gX = ballOn.X - dxBallOnGhost;
double gY = ballOn.Y - dyBallOnGhost;
double dxGhostCue = balls[0].X - gX;
double dyGhostCue = balls[0].Y - gY;
double hGhostCue = Math.Sqrt(dxGhostCue * dxGhostCue + dyGhostCue * dyGhostCue);
//distances between ball on center and cue ball center
double dxBallOnCueBall = ballOn.X - balls[0].X;
double dyBallOnCueBall = ballOn.Y - balls[0].Y;
double hBallOnCueBall = Math.Sqrt(dxBallOnCueBall *
dxBallOnCueBall + dyBallOnCueBall * dyBallOnCueBall);
//discards difficult ghost balls
if (Math.Sign(dxPocketBallOn) == Math.Sign(dxBallOnCueBall) &&
Math.Sign(dyPocketBallOn) == Math.Sign(dyBallOnCueBall))
{
Ball ghostBall = new Ball(i.ToString(), null,
(int)gX, (int)gY, "", null, null, 0);
ghostBalls.Add(ghostBall);
i++;
}
}
return ghostBalls;
}
有些鬼球可能很难或无法到达,因为它们位于目标球的后面.这些鬼球将由计算机丢弃:(Some ghost balls may be difficult or impossible to reach, because they lie behind the object ball. These ghost balls are to be discarded by the computer:)
//discards difficult ghost balls
if (Math.Sign(dxPocketBallOn) == Math.Sign(dxBallOnCueBall) &&
Math.Sign(dyPocketBallOn) == Math.Sign(dyBallOnCueBall))
{
Ball ghostBall = new Ball(i.ToString(), null, (int)gX, (int)gY, "", null, null, 0);
ghostBalls.Add(ghostBall);
i++;
}
然后,计算机必须从其余的鬼球中选择一个(有时计算机很幸运,有时不是.).(The computer must then choose one among the remaining ghost balls (sometimes the computer is lucky, sometimes it is not…).)
private Ball GetRandomGhostBall(List ballOnList)
{
Ball randomGhostBall = null;
List ghostBalls = new List();
foreach (Ball ballOn in ballOnList)
{
List tempGhostBalls = GetGhostBalls(ballOn);
foreach (Ball ghostBall in tempGhostBalls)
{
ghostBalls.Add(ghostBall);
}
}
int ghostBallCount = ghostBalls.Count;
if (ghostBallCount > 0)
{
Random rnd = new Random(DateTime.Now.Second);
int index = rnd.Next(ghostBallCount);
randomGhostBall = ghostBalls[index];
}
return randomGhostBall;
}
图8.鬼球.(Figure 8. Ghost Balls.)## 未来版本(Future Releases)
- 多人游戏功能.(Multiplayer features.)
- 多计算机功能(定义为:WCF,Remoting,Skype等).(Multi-machine features (to be defined: WCF, Remoting, Skype, etc.).)
历史(History)
- 2009-11-29:第一个版本.(2009-11-29: First version.)
- 2009-12-04:文章已更新.(2009-12-04: Article updated.)
- 2009-12-09:错误修复.(2009-12-09: Bug fixes.)
- 2009年12月12日:改进了A.I.,错误修复.(2009-12-12: Improved A.I., bug fixes.)
许可
本文以及所有相关的源代码和文件均已获得The Code Project Open License (CPOL)的许可。
C# C#2.0 WinXP Vista .NET2.0 Win32 Visual-Studio VS2005 Design Dev 新闻 翻译