你的位置:首页 > 信息动态 > 新闻中心
信息动态
联系我们

《C#零基础入门之百识百例》(四十)方法应用 -- 推箱子游戏 -- 代码解析

2021-12-5 23:17:04

C#零基础入门 函数应用 -- 推箱子游戏

  • 前言
  • 效果展示
  • 一,关卡设计
  • 二,地图初始化
  • 三,玩家移动
  • 四,玩家推箱子
  • 五,游戏结束
  • 六,源码分享

前言

本文属于C#零基础入门之百识百例系列文章。此系列文章旨在为学习C#语言的童鞋提供一套系统的学习路径。此系列文章都会通过【知识点】【练习题】的形式呈现。有任何问题,你都可以通过评论私信等方式找到我,我会一对一解答你的问题。


系列文章目录: 导图
《C#零基础入门之百识百例》 目录文章传送门


效果展示

请添加图片描述


一,关卡设计

第一关效果图

首先我们要确定一下数组值的含义和其对应的表示:

数值含义符号
0空格“ ” 空字符串
1墙体■ 实心正方形
2箱子□ 空心正方形
3小人人 汉字
4目标☆ 空心五角星
5完成★实心五角星

基本定义就是这样,详解在使用数组实例一文中: 《C#零基础入门之百识百例》(三十)数组应用 – 推箱子游戏 – 地图初始化,这里不在赘述。


二,地图初始化

主要逻辑:

  1. 定义两个二维数组表示关卡地图,map带有玩家(3),targeMap不带玩家(3)
  2. 遍历map将其值对应的地图含义转换为对应的图形,打印出来
/// <summary>
/// 推箱子
/// 地图数值含义:0:空地,1:墙体,2:箱子,3:人,4:目标,5:完成
/// </summary>
class Sokoban
{        
	static void Main(string[] args)
    {
       // 关卡初始化
       InitMap();
       // 刷新地图
	   UpdateMap();

	   Console.ReadLine();
	}

    // 本关卡地图
    static int[,] map;
    // 目标地图
    static int[,] targeMap;

    //玩家的初始坐标
    static int y = 4, x = 5;
    /// <summary>
    /// 关卡初始化 -- 关卡数组,玩家位置
    /// </summary>
    static void InitMap()
    {
        y = 4;
        x = 5;

        map = new int[8, 9]
        {
                {0,0,0,1,1,1,0,0,0},
                {0,0,0,1,4,1,0,0,0},
                {0,0,0,1,0,1,1,1,1},
                {0,1,1,1,2,0,2,4,1},
                {0,1,4,0,2,3,1,1,1},
                {0,1,1,1,1,2,1,0,0},
                {0,0,0,0,1,4,1,0,0},
                {0,0,0,0,1,1,1,0,0}
        };

        targeMap = new int[8, 9]
        {
                {0,0,0,1,1,1,0,0,0},
                {0,0,0,1,4,1,0,0,0},
                {0,0,0,1,0,1,1,1,1},
                {0,1,1,1,2,0,2,4,1},
                {0,1,4,0,2,0,1,1,1},
                {0,1,1,1,1,2,1,0,0},
                {0,0,0,0,1,4,1,0,0},
                {0,0,0,0,1,1,1,0,0}
        };
    }

	/// <summary>
    /// 刷新地图显示
    /// </summary>
    static void UpdateMap()
    {
        // 清屏
        Console.Clear();
        Console.WriteLine("-------- 推箱子 --------");
        Console.WriteLine();

        // 遍历数组 -- 打印新地图
        for (int i = 0; i < map.GetLength(0); i++)
        {
            for (int j = 0; j < map.GetLength(1); j++)
            {
                if (map[i, j] == 0)
                {
                    Console.Write(" ");
                }
                if (map[i, j] == 1)
                {
                    Console.Write("■");
                }
                if (map[i, j] == 2)
                {
                    Console.Write("□");
                }
                if (map[i, j] == 3)
                {
                    Console.Write("人![请添加图片描述](https://img-blog.csdnimg.cn/6a07bce3885340c6aa0b038e46858af9.gif)
");
                }
                if (map[i, j] == 4)
                {
                    Console.Write("☆");
                }
                if (map[i, j] == 5)
                {
                    Console.Write("★");
                }
            }
            Console.WriteLine();
        }

        Console.WriteLine();
        Console.WriteLine("按 ↑←↓→ 控制玩家移动");
        Console.WriteLine("输入 G 重新开始");
    }
}

三,玩家移动

实现逻辑:

  1. 用户通过 “↑←↓→“ 来控制玩家移动,所以需要在循环中,检测键盘输入,然后处理对应的键盘输入逻辑
  2. 需要注意玩家移动的方向,对数组进行的计算逻辑。比如向上移动,则是对y-1; 这里需要理解一下
  3. 进行数组越界校验,不满足的值不进行处理,避免程序报错。

模拟玩家移动逻辑:

/// <summary>
/// 移动函数
/// </summary>
/// <param name="direction">移动方向</param>
static void Move(ConsoleKey direction)
{
    // 本次玩家移动位置
    int ox = 0, oy = 0;

    // 根据方向进行偏移计算
    switch (direction)
    {
        case ConsoleKey.UpArrow:
            oy--;
            break;
        case ConsoleKey.DownArrow:
            oy++;
            break;
        case ConsoleKey.LeftArrow:
            ox--;
            break;
        case ConsoleKey.RightArrow:
            ox++;
            break;
        default:
            return;
    }

	// 玩家移动走 -- 还原地图
    map[y, x] = targeMap[y, x];
    y += oy;
    x += ox;
    // 做越界判断
    if (y < map.GetLength(0) && y > 0 && x < map.GetLength(1) && x > 0)
    {
        map[y, x] = 3;
    }
}

修改Main函数如下,即可运行输入控制玩家移动

static void Main(string[] args)
{
    // 关卡初始化
    InitMap();

    // 循环 -> 等待用户输入后执行一次
    while (true)
    {
        // 刷新地图
        UpdateMap();
        // 接收用户输入操作
        ConsoleKeyInfo keyInfo = Console.ReadKey();
        // 主角移动
        Move(keyInfo.Key);
    }
    
    Console.ReadLine();
}

移动效果:
请添加图片描述


四,玩家推箱子

玩家推箱子可以分解为两个步骤:**1. 玩家向前移动一步;2. 箱子向前移动一步。**而这里两个步骤又有各种条件限制,如前面是墙箱子不能动,箱子前面是墙不能动等等…

整理后的移动情况如下:(为方便描述将A1作为玩家移动的下一个目标点,A2作为下两个目标点)

  1. A1是空地目标 --> 可以移动
  2. A1是墙体 --> 不能移动
  3. A1是箱子到目标点的箱子 --> 可以移动 --> 此时需要看能不能推动箱子,即A2点情况
    - A2是空地目标点 --> 可以移动
    - A2是墙体箱子达到目标的箱子 --> 不能移动

注意:当可以移动时,还需要考虑当前玩家位置是否目标位置,若是目标位置还需要还原会目标,若不是则还原为空格即可。


移动代码修改如下:

/// <summary>
/// 移动函数
/// </summary>
/// <param name="direction">移动方向</param>
static void Move(ConsoleKey direction)
{
    // 玩家要移动的下一个位置的x,y
    int nx1 = x, ny1 = y;
    // 同方向的移动下两个位置的x,y
    int nx2 = x, ny2 = y;
    // 本次玩家移动位置
    int ox = 0, oy = 0;

    // 根据方向进行偏移计算
    switch (direction)
    {
        case ConsoleKey.UpArrow:
            ny1 -= 1;
            ny2 -= 2;
            oy--;
            break;
        case ConsoleKey.DownArrow:
            ny1 += 1;
            ny2 += 2;
            oy++;
            break;
        case ConsoleKey.LeftArrow:
            nx1 -= 1;
            nx2 -= 2;
            ox--;
            break;
        case ConsoleKey.RightArrow:
            nx1 += 1;
            nx2 += 2;
            ox++;
            break;
        default:
            return;
    }


    // 玩家的下一个坐标为空地,进行移动
    if (map[ny1, nx1] == 0 || map[ny1, nx1] == 4)
    {
        map[ny1, nx1] = 3;
        map[y, x] = 0;
        // 玩家现在的坐标是目标点的坐标 -- 还原回去
        if (targeMap[y, x] == 4)
            map[y, x] = 4;
        else
            map[y, x] = 0;
        y += oy;
        x += ox;
    }
    // 玩家的下一个坐标为墙 不做处理
    else if (map[ny1, nx1] == 1)
    {
        return;
    }
    // 玩家的下一个坐标为未到达目标的箱子或到达目标的箱子
    else if (map[ny1, nx1] == 2 || map[ny1, nx1] == 5)
    {           
        // 箱子的下一个目标为空地
        if (map[ny2, nx2] == 0)
        {
            map[ny2, nx2] = 2;
            map[ny1, nx1] = 3;
            // 玩家现在的坐标是目标点的坐标 -- 还原回去
            if (targeMap[y, x] == 4)
                map[y, x] = 4;
            else
                map[y, x] = 0;
            y += oy;
            x += ox;
        }
        // 箱子的下一个目标为目标点
        else if (map[ny2, nx2] == 4)
        {
            map[ny2, nx2] = 5;
            map[ny1, nx1] = 3;
            // 玩家现在的坐标是目标点的坐标 -- 还原回去
            if (targeMap[y, x] == 4)
                map[y, x] = 4;
            else
                map[y, x] = 0;
            y += oy;
            x += ox;
        }
        else
        {
            // 箱子的下一个目标为墙或空箱子或到达目标的箱子 -- 不做处理
            //if (map[ny2, nx2] == 1 || map[ny2, nx2] == 2 || map[ny2, nx2] == 5)
        }
    }
}

五,游戏结束

判断游戏结束逻辑就比较简单了,当地图中没有箱子目标点时,即可认为是游戏成功。代码实现如下:

/// <summary>
/// 游戏结束 -- 地图中没有箱子和目标点
/// </summary>
/// <returns></returns>
static bool IsGameOver()
{
    for (int i = 0; i < map.GetLength(0); i++)
    {
        for (int j = 0; j < map.GetLength(1); j++)
        {
            if (map[i, j] == 2 || map[i, j] == 4)
            {
                return false;
            }
        }
    }
    return true;
}

游戏结束后,可处理根据用户操作进行重开或者进行下一关卡(本文未实现,其实逻辑不能,只是将地图设置为下一关卡即可)

修改Main函数如下:

static void Main(string[] args)
{
    // 关卡初始化
    InitMap();


    // 循环 -> 等待用户输入后执行一次
    while (true)
    {
        // 刷新地图
        UpdateMap();

        // 游戏成功
        if (IsGameOver())
        {
            Console.WriteLine("======== 恭喜过关 ========");
            // Console.WriteLine("按下任意键重玩...");
            // todo...下一关
            // Console.ReadKey();
            //break;
        }

        // 接收用户输入操作
        ConsoleKeyInfo keyInfo = Console.ReadKey();

        // 重玩
        if (keyInfo.Key == ConsoleKey.G)
        {
            // 关卡初始化
            InitMap();
            // 刷新地图
            UpdateMap();
        }
        else
        {
            // 主角移动
            Move(keyInfo.Key);
        }
    }

    Console.ReadLine();
}

六,源码分享

上面代码可以组成一个完成的推箱子游戏了,组合后的代码:

/// <summary>
/// 推箱子
/// </summary>
class Sokoban
{       
    /// <summary>
    /// 本关卡地图
    /// 值释义 ->  0:空格; 墙:1■; 箱子:2□; 人:3人; 目标:4☆; 完成:5★
    /// </summary>
    static int[,] map;
    // 目标地图
    static int[,] targeMap;

    //玩家的初始坐标
    static int y = 4, x = 5;


    static void Main(string[] args)
    {
        // 关卡初始化
        InitMap();


        // 循环 -> 等待用户输入后执行一次
        while (true)
        {
            // 刷新地图
            UpdateMap();

            // 游戏成功
            if (IsGameOver())
            {
                Console.WriteLine("======== 恭喜过关 ========");
                // Console.WriteLine("按下任意键重玩...");
                // todo...下一关
                // Console.ReadKey();

                break;
            }

            // 接收用户输入操作
            ConsoleKeyInfo keyInfo = Console.ReadKey();

            // 重玩
            if (keyInfo.Key == ConsoleKey.G)
            {
                // 关卡初始化
                InitMap();
                // 刷新地图
                UpdateMap();
            }
            else
            {
                // 主角移动
                Move(keyInfo.Key);
            }
        }


        Console.ReadLine();
    }

    /// <summary>
    /// 关卡初始化 -- 关卡数组,玩家位置
    /// </summary>
    static void InitMap()
    {
        y = 4;
        x = 5;

        map = new int[8, 9]
        {
                {0,0,0,1,1,1,0,0,0},
                {0,0,0,1,4,1,0,0,0},
                {0,0,0,1,0,1,1,1,1},
                {0,1,1,1,2,0,2,4,1},
                {0,1,4,0,2,3,1,1,1},
                {0,1,1,1,1,2,1,0,0},
                {0,0,0,0,1,4,1,0,0},
                {0,0,0,0,1,1,1,0,0}
        };

        targeMap = new int[8, 9]
        {
                {0,0,0,1,1,1,0,0,0},
                {0,0,0,1,4,1,0,0,0},
                {0,0,0,1,0,1,1,1,1},
                {0,1,1,1,2,0,2,4,1},
                {0,1,4,0,2,0,1,1,1},
                {0,1,1,1,1,2,1,0,0},
                {0,0,0,0,1,4,1,0,0},
                {0,0,0,0,1,1,1,0,0}
        };
    }

    /// <summary>
    /// 刷新地图显示
    /// </summary>
    static void UpdateMap()
    {
        // 清屏
        Console.Clear();
        Console.WriteLine("-------- 推箱子 --------");
        Console.WriteLine();

        //打印新地图
        for (int i = 0; i < map.GetLength(0); i++)
        {
            for (int j = 0; j < map.GetLength(1); j++)
            {
                if (map[i, j] == 0)
                {
                    Console.Write("  ");
                }
                if (map[i, j] == 1)
                {
                    Console.Write("■");
                }
                if (map[i, j] == 2)
                {
                    Console.Write("□");
                }
                if (map[i, j] == 3)
                {
                    Console.Write("人");
                }
                if (map[i, j] == 4)
                {
                    Console.Write("☆");
                }
                if (map[i, j] == 5)
                {
                    Console.Write("★");
                }
            }
            Console.WriteLine();
        }

        Console.WriteLine();
        Console.WriteLine("按 ↑←↓→ 控制玩家移动");
        Console.WriteLine("输入 G 重新开始");
    }


    /// <summary>
    /// 移动函数
    /// </summary>
    /// <param name="direction">移动方向</param>
    static void Move(ConsoleKey direction)
    {
        // 玩家要移动的下一个位置的x,y
        int nx1 = x, ny1 = y;
        // 同方向的移动下两个位置的x,y
        int nx2 = x, ny2 = y;
        // 本次玩家移动位置
        int ox = 0, oy = 0;

        // 根据方向进行偏移计算
        switch (direction)
        {
            case ConsoleKey.UpArrow:
                ny1 -= 1;
                ny2 -= 2;
                oy--;
                break;
            case ConsoleKey.DownArrow:
                ny1 += 1;
                ny2 += 2;
                oy++;
                break;
            case ConsoleKey.LeftArrow:
                nx1 -= 1;
                nx2 -= 2;
                ox--;
                break;
            case ConsoleKey.RightArrow:
                nx1 += 1;
                nx2 += 2;
                ox++;
                break;
            default:
                return;
        }


        // 玩家的下一个坐标为空地,进行移动
        if (map[ny1, nx1] == 0 || map[ny1, nx1] == 4)
        {
            map[ny1, nx1] = 3;
            map[y, x] = 0;
            // 玩家现在的坐标是目标点的坐标 -- 还原回去
            if (targeMap[y, x] == 4)
                map[y, x] = 4;
            else
                map[y, x] = 0;
            y += oy;
            x += ox;
        }
        // 玩家的下一个坐标为墙 不做处理
        else if (map[ny1, nx1] == 1)
        {
            return;
        }
        // 玩家的下一个坐标为未到达目标的箱子或到达目标的箱子
        else if (map[ny1, nx1] == 2 || map[ny1, nx1] == 5)
        {
            // 箱子的下一个目标为空地
            if (map[ny2, nx2] == 0)
            {
                map[ny2, nx2] = 2;
                map[ny1, nx1] = 3;
                // 玩家现在的坐标是目标点的坐标 -- 还原回去
                if (targeMap[y, x] == 4)
                    map[y, x] = 4;
                else
                    map[y, x] = 0;
                y += oy;
                x += ox;
            }
            // 箱子的下一个目标为目标点
            else if (map[ny2, nx2] == 4)
            {
                map[ny2, nx2] = 5;
                map[ny1, nx1] = 3;
                // 玩家现在的坐标是目标点的坐标 -- 还原回去
                if (targeMap[y, x] == 4)
                    map[y, x] = 4;
                else
                    map[y, x] = 0;
                y += oy;
                x += ox;
            }
            else
            {
                // 箱子的下一个目标为墙或空箱子或到达目标的箱子 -- 不做处理
                //if (map[ny2, nx2] == 1 || map[ny2, nx2] == 2 || map[ny2, nx2] == 5)
            }
        }
    }

    /// <summary>
    /// 游戏结束 -- 地图中没有箱子和目标点
    /// </summary>
    /// <returns></returns>
    static bool IsGameOver()
    {
        for (int i = 0; i < map.GetLength(0); i++)
        {
            for (int j = 0; j < map.GetLength(1); j++)
            {
                if (map[i, j] == 2 || map[i, j] == 4)
                {
                    return false;
                }
            }
        }
        return true;
    }
}