从无到有写一个C#弹球小游戏(一)

弹球游戏也算是经典小游戏之一,这次我试着自己写一个弹球小游戏。



新建项目:C#->win窗体应用程序->完成,开始写代码。


第一步先想一想这个游戏都有哪些要素:一个球,一个玩家控制的球拍,一系列砖块。他们都需要有哪些性质呢?


球:在屏幕上做直线移动,碰到别的东西反弹。

而反弹是什么呢,反弹就是改变速度方向。如果分解速度的话可以分解成x方向和y方向的速度,因为在这个游戏中球只可能碰到矩形物体,所以不考虑斜面。球只可能碰到与坐标轴平行的边界。这里说的坐标轴是win窗口上的坐标轴,向右为x轴正向,向左为y轴正向,窗口左上角是原点。

所以可以把反弹当作如果碰到与x轴平行的边界,那么与y轴平行的速度的分量变为原来的负值,如果碰到与y轴平行的边界,那么与x轴平行的速度的分量变为原来的负值。


球拍:球拍只可能随着玩家的控制做左右移动,而且碰到移动的边界的话就停止继续往前走。


砖块:砖块静止在场景中,如果被别的物体(球)碰撞就会消失。


因为C#是面向对象的,所以以上的三个东西就是三个class,而且显而易见的是他们有一些共同的性质,所以可以由一个class派生出来。

他们有什么共同的性质呢?

三个东西都得在场景中绘制出来,都可能与别的东西碰撞。

这就是一些性质:位置,边界,绘制,碰撞。


所以基类就应该有这些字段:位置信息,碰撞信息,边界信息。

这些方法:绘图,运动,是否碰撞。

位置,碰撞,边界三个东西说白了就是三个不同的矩形,只不过功能不太一样。

位置矩形确定了这个物体应该在什么地方画,碰撞矩形确定了这个物体怎么与别的东西碰撞。很多人认为这两个矩形应该是一回事,实际上在弹球这个游戏中这两个矩形确实也是一回事,但是为了类的可移植性,还是同时保留这两个信息吧。事实上,我做这个练习是为了以后写一个类似的“坦克大战”小游戏联系一下,毕竟刚开始学C#。

边界其实就是一个移动范围,确定了这个边界之后,这个物体就只能在这个边界范围内移动。也许会有人觉得所有物体共用一个边界就行了,何必每个物体储存自己的边界信息呢,这不是浪费空间吗?在这个游戏中确实也是所有物体用同一个边界信息的,可是在别的地方,比如有一个小怪,默认的状态是在城堡的门口巡逻,来来回回来来回回,这个时候这个信息就有用了,这个小怪的边界就是城堡门口的一片地方。


考虑到这三种信息都是一个矩形,所以我自己写了一个Bounds结构体来封装这个矩形,方便操作,代码如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace 弹球
{
    /// <summary>
    /// 边界:为简化问题,场景中每个物体都有其矩形碰撞边界
    /// </summary>
    public struct Bounds
    {
        /// <summary>
        /// 左上方的顶点X坐标
        /// </summary>
        public int Left;
        /// <summary>
        /// 左上方的顶点Y坐标
        /// </summary>
        public int Top;
        /// <summary>
        /// 宽度
        /// </summary>
        public int Right;
        /// <summary>
        /// 高度
        /// </summary>
        public int Bottom;

        /// <summary>
        /// 构造函数:设置顶点位置,长宽
        /// </summary>
        /// <param name="iX"></param>
        /// <param name="iY"></param>
        /// <param name="iWidth"></param>
        /// <param name="iHeight"></param>
        public Bounds(int iX = 0, int iY = 0, int iWidth = 0, int iHeight = 0)
        {
            Left = iX;
            Top = iY;
            Right = iX + iWidth;
            Bottom = iY + iHeight;
        }

        /// <summary>
        /// 构造函数:根据另一边界信息创建边界
        /// </summary>
        /// <param name="Other"></param>
        public Bounds(Bounds Other)
        {
            Left = Other.Left;
            Top = Other.Top;
            Right = Other.Right;
            Bottom = Other.Bottom;
        }

        /// <summary>
        /// 设置顶点位置,长宽,经计算得到上下左右四个值
        /// </summary>
        /// <param name="iX"></param>
        /// <param name="iY"></param>
        /// <param name="iWidth"></param>
        /// <param name="iHeight"></param>
        public void SetBounds(int iX, int iY, int iWidth = 0, int iHeight = 0)
        {
            Left = iX;
            Top = iY;
            Right = iX + iWidth;
            Bottom = iY + iHeight;
        }

        /// <summary>
        /// 根据另一边界设置边界
        /// </summary>
        /// <param name="Other"></param>
        public void SetBounds(Bounds Other)
        {
            Left = Other.Left;
            Top = Other.Top;
            Right = Other.Right;
            Bottom = Other.Bottom;
        }

        /// <summary>
        /// 向右移动(参数为负时向左移动)
        /// </summary>
        /// <param name="iDistance"></param>
        public void MoveRight(int iDistance)
        {
            Left += iDistance;
            Right += iDistance;
        }

        /// <summary>
        /// /// <summary>
        /// 向下移动(参数为负时向上移动)
        /// </summary>
        /// <param name="distance"></param>
        /// </summary>
        /// <param name="iDistance"></param>
        public void MoveDown(int iDistance)
        {
            Top += iDistance;
            Bottom += iDistance;
        }

        /// <summary>
        /// 向iVvector所指的方向移动,距离为iSpeed
        /// </summary>
        /// <param name="iVector"></param>
        /// <param name="iSpeed"></param>
        public void Move(vector2D iVector, int iSpeed)
        {
            Left += (int)Math.Ceiling(iVector.X * iSpeed);
            Right += (int)Math.Ceiling(iVector.X * iSpeed);

            Top += (int)Math.Ceiling(iVector.Y * iSpeed);
            Bottom += (int)Math.Ceiling(iVector.Y * iSpeed);
        }
    }

    /// <summary>
    /// 枚举值方向,有五个可能的值:None, Left, Up, Right, Down
    /// </summary>
    enum Direction { None = 0, Left = 1, Up = 2, Right = 3, Down = 4 };

}

因为后main会涉及到物体移动的问题,而一个矩形移动至少会涉及到两个值的改变,比如左右移动至少会改变left和right边界,所以封装好移动的几个函数方便使用。


方向的枚举值有五个,但是在这个程序中只会用到三个(表示拍子的移动方向),代码的冗余是挺令人不爽,但是考虑到”坦克大战“中坦克的移动方向会有五个,所以也不难忍受。


上面有一个vector2D的结构体,这其实就是矢量,因为球的速度是矢量,包括x方向的和y方向的速度,所以我写了这么一个结构体如下:

    /// <summary>
    /// 二维向量,用来表示平面上的方向,只有方向,没有大小
    /// </summary>
    public struct vector2D
    {
        /// <summary>
        /// 横向的值
        /// </summary>
        private double x;

        /// <summary>
        /// 获得横向的值
        /// </summary>
        public double X
        {
            get { return x; }
        }

        /// <summary>
        /// 将横向值取反
        /// </summary>
        public void NegateX()
        {
            x = -x;
        }

        /// <summary>
        /// 纵向的值
        /// </summary>
        private double y;

        /// <summary>
        /// 将纵向值取反
        /// </summary>
        public void NegateY()
        {
            y = -y;
        }

        /// <summary>
        /// 获得纵向的值
        /// </summary>
        public double Y
        {
            get { return y; }
        }


        /// <summary>
        /// 构造函数:向量为单位长度,参数只代表方向
        /// </summary>
        /// <param name="iX"></param>
        /// <param name="iY"></param>
        public vector2D(double iX, double iY)
        {
            double length = Math.Sqrt(iX*iX + iY*iY);
            x = iX/length;
            y = iY/length;
        }

        /// <summary>
        /// 注意:向量为单位长度
        /// </summary>
        /// <param name="iX"></param>
        /// <param name="iY"></param>
        public void setValue(double iX, double iY)
        {
            double length = Math.Sqrt(iX * iX + iY * iY);
            x = iX / length;
            y = iY / length;
        }
    }

经过上面的铺垫,那三个类的父类代码终于出来了,如下:

using System;
using System.Drawing;

namespace 弹球
{
    /// <summary>
    /// 放置在场景中所有物体的父类,包含位置边界、碰撞边界和移动边界等基本信息。
    /// </summary>
    abstract class Actor
    {
        public Actor()
        { }

        /// <summary>
        /// 位置信息:绘图时的信息
        /// </summary>
        public Bounds PositionBounds;

        /// <summary>
        /// 碰撞信息:物体与其他物体发生碰撞时的信息
        /// </summary>
        public Bounds CollisionBounds;

        /// <summary>
        /// 移动边界信息:物体无论如何移动都不会移出此范围
        /// </summary>
        public Bounds MoveBounds;

        /// <summary>
        /// 构造函数:设置Actor的位置
        /// </summary>
        /// <param name="iX"></param>
        /// <param name="iY"></param>
        public Actor(int iX, int iY)
        {
            PositionBounds.SetBounds(iX, iY);
            CollisionBounds.SetBounds(iX, iY);
        }

        /// <summary>
        /// Actor的绘图函数,每个Actor有自己的绘图函数,当要绘制它的时候调用这个函数
        /// </summary>
        /// <param name="g"></param>
        abstract public void Draw(Graphics g);

        abstract public void Update();

        /// <summary>
        /// 向右移动(参数为负时向左移动)
        /// </summary>
        /// <param name="distance"></param>
        public void MoveRight(int distance)
        {
            PositionBounds.MoveRight(distance);
            CollisionBounds.MoveRight(distance);
        }

        /// <summary>
        /// 向下移动(参数为负时向上移动)
        /// </summary>
        /// <param name="distance"></param>
        public void MoveDown(int distance)
        {
            PositionBounds.MoveDown(distance);
            CollisionBounds.MoveDown(distance);
        }

        /// <summary>
        /// 检测是否与另一物体碰撞:
        /// </summary>
        /// <param name="Other"></param>
        /// <returns></returns>
        public Boolean IsCollisionDirectionWith(Actor Other)
        {
            return Actor.IsCollision(this, Other);
        }

        /// <summary>
        /// 静态方法:检测两个Actor是否相碰撞,如果碰撞返回true
        /// </summary>
        /// <param name="a"></param>
        /// <param name="b"></param>
        /// <returns></returns>
        public static Boolean IsCollision(Actor a, Actor b)
        {
            if (a.CollisionBounds.Right < b.CollisionBounds.Left ||
                a.CollisionBounds.Bottom < b.CollisionBounds.Top ||
                b.CollisionBounds.Right < a.CollisionBounds.Left ||
                b.CollisionBounds.Bottom < a.CollisionBounds.Top)
            {
                return false;
            }
            else
            {
                return true;
            }
        }
    }
}
### 回答1: 这是一个简单的Go语言弹球小游戏: package main import ( "fmt" "time" ) func main() { var x, y int fmt.Println("欢迎来到弹球小游戏!") for { fmt.Println("请输入x坐标:") fmt.Scan(&x) fmt.Println("请输入y坐标:") fmt.Scan(&y) if x == 0 || y == 0 { fmt.Println("游戏结束!") break } // 延时1秒 time.Sleep(time.Second) // 判断x和y坐标是否在边界内 if x < 0 || x > 9 || y < 0 || y > 9 { fmt.Println("坐标超出边界,请重新输入!") continue } // 判断是否碰到边界 if x == 0 || x == 9 || y == 0 || y == 9 { fmt.Println("你碰到了边界,游戏结束!") break } // 判断是否碰到障碍物 if (x == 1 && y == 1) || (x == 8 && y == 1) || (x == 1 && y == 8) || (x == 8 && y == 8) { fmt.Println("你碰到了障碍物,游戏结束!") break } fmt.Println("你的坐标是:", x, y) } } ### 回答2: 弹球小游戏是一种经典的游戏,在开发中可以使用Go语言来实现。Go语言是一门简洁、高效的编程语言,具有并发性能优秀的特点,非常适合用于开发小型游戏。 在Go语言中,可以使用一些库来实现弹球小游戏的功能,例如使用`github.com/nsf/termbox-go`库来创建一个终端界面,并处理用户输入。此外,还可以使用Go语言的内置功能来处理游戏的物理引擎和游戏逻辑。 游戏的主要功能包括: - 创建一个终端界面,并初始化游戏场景。 - 在界面中绘制一个球和一个挡板,分别代表弹球和玩家控制的挡板。 - 实现球在界面中的移动和碰撞反弹效果。 - 实现挡板的移动,通过控制挡板来夹击球。 - 添加得分系统和游戏结束判断。 游戏的具体逻辑如下: 1. 初始化终端界面,并绘制游戏场景,球和挡板的位置。 2. 不断监听玩家输入,根据输入控制挡板的左右移动。 3. 更新球的位置,判断球是否与边界或挡板发生碰撞。如果碰撞,根据碰撞位置和角度来计算反弹的方向和角度。 4. 当球与底部边界碰撞时,游戏结束,显示得分。 5. 根据游戏规则,计算得分并显示。 6. 重复步骤2到5,直到玩家主动退出游戏。 通过使用Go语言的相关库和语言特性,可以很容易地实现一个简单而有趣的弹球小游戏。 ### 回答3: 弹球小游戏是一种经典的游戏,通过控制一个滑板反弹球体,以尽可能长时间保持球体不掉落而获得得分。下面是用Go语言编一个简单的弹球小游戏的步骤。 首先,我们需要导入必要的Go包。例如,使用`fmt`包来处理输入和输出,使用`github.com/nsf/termbox-go`包来处理终端界面。 然后,我们需要定义游戏所需的结构体和变量。这可能包括球体的位置和速度,滑板的位置,分数和游戏状态等。 接下来,我们需要初始化游戏界面,包括设置终端的大小和绘制游戏元素,如球体和滑板。 然后,我们需要编游戏的主循环。在每一次循环中,我们需要处理用户的输入(例如通过键盘控制滑板的移动),更新游戏元素的状态(例如更新球体的位置)并重新绘制游戏界面。 在游戏的主循环中,我们还需要处理与边界的碰撞检测,如果球体触碰到边界,则需要改变球体的速度方向。 我们还需要处理球体与滑板的碰撞检测。如果球体触碰到滑板,则需要改变球体的速度方向。 在游戏的主循环中,我们还需要处理游戏的结束条件,例如当球体掉落到底部时,游戏结束,显示最终的得分。 最后,我们需要运行游戏的主循环并保持运行,直到游戏结束。在游戏结束后,我们可以处理用户的输入,询问是否重新开始游戏。 通过以上步骤,我们就可以用Go语言一个简单的弹球小游戏了。当然,这只是一个简单的思路,实际的实现过程中还需要一些其他的具体细节和处理逻辑。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值