骑士飞行棋 - C#控制台小游戏

01 概要设计

 

1、显示欢迎界面

2、提示玩家A、B输入名字(判断玩家输入的名字是否合法)

3、用户输入合法的名字后,显示给用户

4、清屏,重新绘制界面,提示游戏开始

5、显示说明和图例

6、绘制地图之前,初始化关卡

7、绘制地图

8、玩家A和B轮流掷骰子

9、关卡效果

10、一方胜利,显示提示信息,游戏结束


 

02 详细设计

 

2.1 显示欢迎界面

       用多个Console.WriteLine();  语句在屏幕上显示一个简单界面。

并将此段代码写成方法,方便多次调用。

 

2.2 提示玩家A、B输入名字(判断玩家输入的名字是否合法)

  2.2.1  定义一个int[] 来存储用户输入的玩家A和玩家B的名字

  2.2.2  判断用于输入的名字是否合法

        1、判断A的名字是否为空。

    若为用户输入为空,提示用户重新输入;若不为空,继续下一步。此处设置while循环,以便重新输入。

        2、判断B的名字是否为空,并且不能与A重名。

若用户输入为空,提示用户重新输入;若用户输入与A重名,提示用户重新输入,知道输入合法为止。此处亦设置while循环。

 

2.3 用户输入合法的名字后,显示给用户

       若用户输入的名字合法,用Console.WriteLine() 语句显示用户刚才输入的A和B的名字。

 

2.4 清屏,重新绘制界面,提示游戏开始

       由于控制台界面大小有限,所以要清屏来保持界面的清爽简洁。

       清屏之后重新绘制界面,并提示游戏开始。

2.5 显示说明和图例

       在游戏开始之前,向用户显示说明文字和图例,便于用户更好的了解。

2.6 绘制地图之前,初始化关卡

       1、初始化游戏关卡,即设置每一个格各对应什么样的规则。为了便于区分各种关卡,给每个关卡分配一种小图标。

       2、本游戏设置了4种关卡,分别是幸运轮盘、地雷、暂停一次、时空隧道。

       3、游戏关卡应显示在游戏中的每个元素中,即分配到每个小格中。

       4、在设置和分配关卡之前应该先定义一个int[] 来存储地图的各元素。游戏中定义了一个int[] Map 来存储地图的各个元素。

       在int[] Map数组中,不同元素的值表示的意义不同,如下所示:

    当元素值为1时,输出幸运轮盘 ★

                2,地雷 ●

                3,暂停 △

                4,时空隧道 卍

                0,表示普通 □

       5、在设置关卡时,定义几个新数组,其值为Map[i]的下标i,存储地图中的关卡。

       6、在绘制地图时,需要先获得关卡的值。所以这里设置循环,将这几个数组中的值分别取出,再赋给Map数组。

        此处用for循环遍历数组。

 

2.7 绘制地图

    本游戏地图共100个元素,100个小格。分为3行2列呈S型排列,每行30个元素,每列5个元素。

       1、绘制第一行(0-29格)

              ①先判断A和B是否在这一格元素上

               要想判断A和B是否在第i格上,首先要知道A和B的位置坐标(下标),       所以要定义一个数组存储A和B的下标。

         这里不在方法中直接输出要绘制的元素符号,而是通过传参的方式返  回要绘制的符号。              

       第一种情况:如果A和B都在当前小格上,则返回 < > ;

       第二种情况:如果A在当前小格上,则返回全角符号A,即 result = "A";

    第三种情况:如果B在当前小格上,则返回全角符号B。

       第四种情况:如果A和B都不在当前小格上,则返回关卡图标或正常图标。在此之前需要先做判断。

              ②判断每一格元素的值

       因这里要返回5种元素,故用switch语句。返回0,表示普通 □ ;返回1,表示幸运轮盘 ★;返回2,表示地雷 ●;返回3,表示暂停 △;返回4,表示时空隧道 卍 。

 

       2、此处的列地图,要绘制29个双空格(即1个全角空格) + 1个地图小格

       3、绘制第二行,方法同第1步

       4、绘制第二列

       5、绘制第三行

 

2.8 玩家A和B轮流掷骰子

       2.8.1 掷骰子的流程分析

       A和B轮流掷骰子需要重复操作,故写成循环。循环条件是,A和B的坐标同时小于99。

       由于A和B轮流掷骰子,即A先掷一次,B掷再一次,循环往复。但是当A或B其中一个走到暂停时,就停一次,对方连掷两次。所以此处设置一个标志位来标记玩家是否走到暂停。

       定义一个数组bool[] isStop 来存储这个标志的两个值。默认情况下都为false,即A和B都没有走到暂停。

       ①如果任意一方走到暂停,就将其isStop[i]设为true;

       ②在循环中需要先判断isStop[i]的值,如果为false,就掷骰子,如果为true, 就停止掷骰子一次(即不执行掷骰子的代码),随后立即将其标志的值设为false。

       由于一开始是A先掷骰子,B再掷骰子,故在循环中应先判断A是否掷骰子的,后判断B是否掷骰子。

       还应注意,如果A掷完骰子后的坐标大于99,证明A已经走到地图终点,此时应break跳出循环。

       2.8.2  A或B掷骰子的方法

              1、掷骰子要产生一个[1,6] 的随机数

              2、用一个变量step来存放产生的随机数

              3、当玩家移动坐标改变时,及时更新step。

              4、当坐标改变时,要检测玩家是否踩到了对方或踩到了关卡。

                     这里将检测的代码写成方法,方便多次调用。

                     ①检测当前玩家是否踩到对方,若踩到对方,对方退起点。

           ②若没有踩到对方,则检测当前玩家是否走到某关卡。

             这里用switch语句做判断。

              5、当玩家踩到对方或处于关卡时,输出提示信息。

                     这里定义一个变量string msg 来存储提示信息。

              6、当玩家走到幸运轮盘时,提示让玩家按1或2键选择运气。此时玩家只能按1键或2键。这里增加了一个方法staticvoid ReadInput(int min, int max),检测玩家输入的数字是否在规定范围内。

 

2.9  关卡效果

       1、一方踩到另一方,被踩的一方退回到起点。

       2、幸运轮盘,可以按1或2键选择运气。按1键,与对方交换位置,按2键,轰炸对方,对方退6格。

       3、踩到地雷,退6格。

       4、走到暂停,暂停一次掷骰子。

       5、时空隧道,前进10格。

       6、开辟的隐藏功能键,轮到用户掷骰子时,用户按下ctrl+Tab组合键,然后再按A键,这是程序会默默等待用户输入合法的数字,若用户输入某个数字后回车,就会提示用户掷出了这个数字,对应的玩家会前进相应的格数;若用户未输入任何数字直接回车,则会按正常掷骰子的方法产生一个随机数。

 

2.10  一方胜利,显示提示信息,游戏结束

       在游戏运行的任何阶段,若某个玩家的坐标 >= 99,则该玩家走到终点,提示该玩家胜利,游戏结束。

 

 

03 代码

 

Program.cs 文件

 

usingSystem;

usingSystem.Collections.Generic;

usingSystem.Linq;

usingSystem.Text;

usingSystem.Threading.Tasks;

 

namespace CS骑士飞行棋

{

    class Program

    {

        ///<summary>

        ///高昌绪制作2014年6月-----

        ///2014年11月5日整理

        ///</summary>

        ///<param name="args"></param>

        static voidMain(string[] args)

        {

            ClassMethod cm= new ClassMethod();

 

           cm.ShowUI();  //显示欢迎界面

 

            //提示用户A、B输入名字

            Console.WriteLine("请输入玩家A的名字:");    

            ClassMethod.names[0]= Console.ReadLine();

 

            //设置一个while循环,判断用户A输入的玩家名字是否为空,若为空重新输入

            while (ClassMethod.names[0]== "")

            {

                Console.WriteLine("玩家A的名字不能为空,请重新输入!");

                ClassMethod.names[0]= Console.ReadLine();

            }

            //提示用户A输入名字

            Console.WriteLine("请输入玩家B的名字:");

            ClassMethod.names[1]= Console.ReadLine();

            //设置while循环,并判断玩家B名字是否为空或是否与玩家A重名,若为空或与A重名则提示重新输入

            while (ClassMethod.names[1]== "" || ClassMethod.names[1] == ClassMethod.names[0] || ClassMethod.names[1] == "")

            {

                if (ClassMethod.names[1]== "")

                {

                    Console.WriteLine("玩家B的名字不能为空,请重新输入!");

                }

                else

                {

                   Console.WriteLine("该名字已被占用,请重新输入!");

                }

                ClassMethod.names[1]= Console.ReadLine();

            }

 

            //显示给用户刚才输入的名字

            Console.WriteLine("玩家A的名字为 {0} ,玩家B的名字为 {1} ,欢迎进入游戏!", ClassMethod.names[0], ClassMethod.names[1]);  //显示玩家名字

            Console.WriteLine("按任意键继续……");

            Console.ReadKey();

           

            //清屏,重新绘制界面,提示游戏开始

            Console.Clear();     //清屏,重新绘制界面

           cm.ShowUI();

            Console.WriteLine("{0}用A表示    {1}用B表示   如果A和B在同一位置,用<>表示。", ClassMethod.names[0], ClassMethod.names[1]);

           

           cm.InitialMap();    //初始化地图关卡

           cm.DrawMap();       //绘制地图

            Console.WriteLine("按任意键游戏开始...");

            Console.ReadLine();

 

 

 

            //A和B轮流掷骰子(设置循环)

            //当A或B的值>=99时,结束循环。    循环条件:A和B的坐标同时小于99。

            while (ClassMethod.playerPos[0]< 99 && ClassMethod.playerPos[1] < 99)

            {

                //判断A是否应该掷骰子

                if(ClassMethod.isStop[0]== false)

                {

                   cm.Action(0);   //掷骰子

                }

                else

                {

                    ClassMethod.isStop[0] = false;

                }

                if (ClassMethod.playerPos[0]>= 99)

                {

                   break;

                }

                //判断B是否掷骰子

                if (ClassMethod.isStop[1]== false)

                {

                   cm.Action(1);   //掷骰子

                }

                else

                {

                   ClassMethod.isStop[1] = false;

                }

            }

 

 

            Console.Clear();

           cm.DrawMap();

            if (ClassMethod.playerPos[0] >= 99)

            {

                Console.WriteLine("{0}先走到了终点,获得了胜利!", ClassMethod.names[0]);

            }

            else if(ClassMethod.playerPos[1]>= 99)

            {

                Console.WriteLine("{0}先走到了终点,获得了胜利!", ClassMethod.names[1]);

            }

 

            Console.WriteLine("游戏结束...     --- Tonyc制作 2014.11.05 ---");

            Console.WriteLine("按任意键退出...");

           

 

            Console.ReadKey();

        }

    }

}

 

 

 

 

 

 

 

 

ClassMethod.cs 文件

 

usingSystem;

usingSystem.Collections.Generic;

usingSystem.Linq;

usingSystem.Text;

usingSystem.Threading.Tasks;

 

namespace CS骑士飞行棋

{

    class ClassMethod

    {

 

        //定义int[]Map,存储地图各元素

        /*数组的下表为0的元素对应地图上的第1格,下标为1的元素对应第2格……下标为n的元素

          对应第(n+1)格。 */

 

        /* 在int[]Map数组中,不同元素的值表示的意义不同,如下:

           元素值为1时,输出幸运轮盘 ★

                    2,地雷 ●

                    3,暂停 △

                    4,时空隧道 卍

                    0,表示普通 □

       */

        public static int[]Map = new int[100];       //定义int[]Map,存储地图各元素

 

        public static string[]names = new string[2];    //定义string[],存储玩家A和玩家B的名字

                                                        //names[0]存储玩家A的名字,names[1]存储玩家B的名字

 

        public static int[]playerPos = { 0, 0 };   //定义一个数组,存储A和B的坐标(下标),playerPos[0]表示A,playerPos[1]表示B。

                                                   // 初始化A和B的下标为0,都在第1格。

                                                   //static int[]playerPos = { 1, 5 };   //测试

 

        public static bool[]isStop = { false, false };//isStop[0]=true表示A上次走到了暂停,isStop[1]=true表示B上次走到了暂停。如果走到了暂停,则设置其值为true。

 

 

 

 

 

 

        ///<summary>

        ///显示飞行棋欢迎界面

        ///</summary>

        public voidShowUI()

        {

            Console.WriteLine("************************************************************");

            Console.WriteLine("*                                                         *");

            Console.WriteLine("*               欢迎来到     骑 士 飞 行 棋               *");

            Console.WriteLine("*                                                         *");

            Console.WriteLine("************************************************************");

        }

 

 

        ///<summary>

        ///初始化地图关卡

        ///定义几个新数组,其值为Map[i]的下标i,存储地图中的关卡

        ///</summary>

        public voidInitialMap()

        {

            int[]luckTurn = { 6, 23, 40, 55, 69, 83 };//幸运轮盘★ Map[i] = 1

            int[]landMine = { 5, 13, 17, 33, 38, 50, 64, 80, 94 };//地雷●  Map[i] = 2

            int[]pause = { 9, 27, 60, 93 };//暂停坐标△ Map[i] = 3

            int[]timeTunnel = { 20, 25, 45, 63, 72, 88, 90 };//时空隧道卍  Map[i] = 4

 

            //设置循环,将各关卡的坐标取出,再赋给Map数组

            for (int i =0; i < luckTurn.Length; i++)

            {

                int pos= luckTurn[i];

               Map[pos] = 1;             //Map[pos]存储地图元素;  这里的pos为其下标  

            }

            for (int i =0; i < landMine.Length; i++)

            {

               Map[landMine[i]] = 2;

            }

            for (int i =0; i < pause.Length; i++)

            {

               Map[pause[i]] = 3;

            }

            for (int i =0; i < timeTunnel.Length; i++)

            {

               Map[timeTunnel[i]] = 4;

            }

        }

 

 

        ///<summary>

        ///绘制地图

        ///</summary>

        ///说明:地图为100个元素,100小格。分为3行2列呈S型排列,每行30个元素,每列5个元素。

        public voidDrawMap()

        {

            //图例

            Console.WriteLine("图例: 幸运轮盘:★   地雷:●   暂停:△   时空隧道:卍   ");

 

            //1、绘制第一行(0-29格):①先判断A和B是否在这一格元素上②考虑每一格元素的值

            for (int i =0; i <= 29; i++)

            {

                Console.Write(GetMapString(i));

            }

            //第一行绘制完毕

            Console.WriteLine();  //换行

 

            //2、绘制第一列

            for (int i =30; i <= 34; i++)

            {

                //这里的列地图,要绘制29个双空格(即1个全角空格) + 1个地图小格

                //绘制29个双空格

                for (int j =0; j <= 28; j++)

                {

                   Console.Write("  ");      //因在行中绘制空格,所以不换行

                }

                //第一列绘制完毕

                //Console.WriteLine(GetMapString(i));

                string str= GetMapString(i);

                Console.WriteLine(str);

            }

            //3、绘制第二行

            for (int i =64; i >= 35; i--)

            {

                Console.Write(GetMapString(i));

            }

            Console.WriteLine();    //第二行绘制完毕

            //4、绘制第二列

            for (int i =65; i <= 69; i++)

            {

                Console.WriteLine(GetMapString(i));    //第二列绘制完毕

            }

            //5、绘制第三行

            for (int i =70; i <= 99; i++)

            {

                Console.Write(GetMapString(i));

            }

            Console.WriteLine();                  //第三行绘制完毕

            Console.ResetColor();                 //重置前景色,恢复正常

        }

 

 

 

        ///<summary>

        ///获得第pos坐标上应该绘制的图案,即判断A、B和关卡是否在当前坐标上

        ///</summary>

        ///<param name="pos">要绘制的坐标</param>

        ///<returns>result</returns>

        static stringGetMapString(int pos)

        {

            stringresult = "";

 

            /*①先判断A和B是否在当前要画的第i格上

              ②再判断每一格的值

            */

            //要想判断A和B是否在第i格上,首先要知道A和B的位置坐标(下标),所以要定义一个数组存储A和B的下标

            //这里不在方法中直接输出,而是通过传参的方式返回要绘制的符号

            if(playerPos[0] == pos && playerPos[1] == pos)

            {

                //Console.ForegroundColor 属性用来设置前景色

                Console.ForegroundColor= ConsoleColor.Red;

               result = "<>";

            }

            else if(playerPos[0] == pos)  //判断A在当前要绘制的格上

            {

                Console.ForegroundColor= ConsoleColor.Red;

               result = "A";  //此处用全角符号。

            }

            else if(playerPos[1] == pos)  //判断B在当前要绘制的格上

            {

                Console.ForegroundColor= ConsoleColor.Red;

                result = "B";

            }

            //②再判断每一格的值

            //这里用switch,因为有多个值对应多个元素

            else

            {

                switch(Map[pos])

                {

                   case 0:

                       Console.ForegroundColor = ConsoleColor.White;

                       result = "□";

                       break;

                   case 1:

                       Console.ForegroundColor = ConsoleColor.Yellow;;

                       result = "★";

                       break;

                   case 2:

                       Console.ForegroundColor = ConsoleColor.Blue;

                       result = "●";

                       break;

                   case 3:

                       Console.ForegroundColor = ConsoleColor.DarkCyan;

                       result = "△";

                       break;

                   case 4:

                       Console.ForegroundColor = ConsoleColor.Green;

                       result = "卍";

                       break;

                }

            }

            returnresult;

        }

 

 

 

 

        ///<summary>

        /// A或B掷骰子的方法

        ///</summary>

        ///<param name="playerNumber">A掷骰子传过来0,B掷骰子传过来1。</param>

        public voidAction(int playerNumber)

        {

            //playerNumber中存的是当前玩家的姓名、坐标、是否暂停 这三个数组的下标(此三个坐标相同)

 

            Random r =new Random();   //产生一个Random类型的变量 r, r用于产生随机数

            intstep = 0;              //用于存放产生的随机数

            string msg= "";           //用于存储用户踩到某关卡所要输出的话

            

            Console.WriteLine();

            Console.WriteLine("玩家{0}按任意键开始掷骰子...",names[playerNumber]);

            ConsoleKeyInfo rec= Console.ReadKey(true);    //获取用户按下的下一个字符或功能键。 按下的键可以选择显示在控制台窗口中。

                                                           //如果为true,则不显示按下的键;否则为 false。

            step =r.Next(1, 7);        //产生一个1—6的随机数 (此数大于等于最小值,小于最大值)

 

 

            //此处开辟了一个隐藏功能组合按键

            //当用户掷骰子前按下ctrl+Tab组合键,再按A键,之后就可以输入1-100中的数字,就是掷骰子出来的数字。

            if(rec.Key==ConsoleKey.Tab&& rec.Modifiers==(ConsoleModifiers .Control))

            {

                ConsoleKeyInfo cc=Console.ReadKey(true);

                if(cc.Key==ConsoleKey.A)

                {

                   step=ReadInput(1,100);

                }

            }

 

            /*

            //三键同按代码

            if(rec.Key == ConsoleKey.Tab && rec.Modifiers ==(ConsoleModifiers.Control| ConsoleModifiers.Shift))

            {

               ConsoleKeyInfo cc = Console.ReadKey(true);

                if(cc.Key == ConsoleKey.A)

                {

                   step =ReadInt (1,100);

                }

 

            }

            

            */

 

 

 

            Console.WriteLine("{0}掷出了{1}",names[playerNumber], step);

            Console.WriteLine("按任意键开始行动...");

            Console.ReadKey(true);

           playerPos[playerNumber] += step;    //玩家行动之后坐标发生改变

 

            //一旦坐标发生改变,就要判断是否越界

           CheckPos();   //检测坐标是否越界

 

            //玩家行动之后,要判断是否踩到对方

            if(playerPos[0] == playerPos[1])

            {

               playerPos[1 - playerNumber] = 0;  //被踩到的一方退回起点

                msg= string.Format("{0}踩到了{1},{1}退回到起点!",names[playerNumber], names[1 - playerNumber]);

            }

            else

            {

                //没踩到对方,则要判断玩家现在的位置是否处于某关卡

                switch(Map[playerPos[playerNumber]])

                {

                   case 0:

                       //普通,没有效果

                       msg = "";

                       break;

                   case 1:

                       //走到了幸运轮盘

                       Console.Clear();

                       DrawMap();

                       Console.WriteLine("玩家{0}掷出了{1}",names[playerNumber ],step);

                       Console.WriteLine("{0}走到了幸运轮盘,请选择运气...",names[playerNumber]);

                       Console.WriteLine("--->1 交换位置     ---> 2 轰炸对方");

                       int plSelect = ReadInput(1, 2);

                       if (plSelect == 1)

                       {

                            inttemp = playerPos[0];

                            playerPos[0] =playerPos[1];

                            playerPos[1] =temp;

                            msg = string.Format("{0}选择了与对方交换位置",names[playerNumber]);

                            Console.WriteLine(msg);

                            Console.WriteLine("按任意键确认...");

                            Console.ReadKey(true);

                       }

                       else

                       {

                            //轰炸对方

                            playerPos[1 - playerNumber]-= 6;

                            CheckPos();

                            msg = string.Format("{0}轰炸了{1},{1}退6格。",names[playerNumber],names[playerNumber]);

                            Console.WriteLine(msg);

                            Console.WriteLine("按任意键确认...");

                            Console.ReadKey(true);

                       }

                       break;

                   case 2:

                       //踩到了地雷

                       Console.Clear();

                       DrawMap();

                       playerPos[playerNumber] -= 6;

                       CheckPos();

                       Console.WriteLine("玩家{0}掷出了{1}", names[playerNumber], step);

                       msg = string.Format("{0}前进{1}踩到了地雷,退了6格!",names[playerNumber],step);

                       Console.WriteLine(msg);

                       Console.WriteLine("按任意键确认...");

                       Console.ReadKey(true);

                       break;

                   case 3:

                       //走到了暂停

                       isStop[playerNumber] = true;

                       msg = string.Format("{0}走到了暂停,暂停掷骰子一次!",names[playerNumber]);

                       Console.WriteLine(msg);

                       Console.WriteLine("按任意键确认...");

                        Console.ReadKey(true);

                       break;

                   case 4:

                       //走到了时空隧道

                       playerPos[playerNumber] += 10;

                       CheckPos();

                       Console.Clear();

                       DrawMap();

                       Console.WriteLine("玩家{0}掷出了{1}", names[playerNumber], step);

                       msg = string.Format("{0}走到了时空隧道,前进10格!",names[playerNumber]);

                       Console.WriteLine(msg);

                       Console.WriteLine("按任意键确认...");

                       Console.ReadKey(true);

                       break;

                }

 

            }

 

 

            Console.Clear();

           DrawMap();

            Console.WriteLine();

            Console.WriteLine("{0}掷出了{1},行动完成。",names[playerNumber],step);

            Console.WriteLine("****** 玩家 {0 }的位置为{1}   玩家 {2} 的位置为{3} ******",names[0],playerPos[0]+1,names[1],playerPos[1]+1);

        }

 

 

 

        ///<summary>

        ///检测输入的数字是否在一定范围内

        ///</summary>

        ///<returns></returns>

        static intReadInput()

        {

            int i =ReadInput(int.MinValue, int.MaxValue);

            return i;

        }

        static intReadInput(int min, int max)

        {

            while (true)

            {

                try

                {

                   int number = Convert.ToInt32(Console.ReadLine());

                   if (number < min || number > max)

                   {

                       Console.WriteLine("只能输入{0}-{1}之间的数,请重新输入!",min,max);

                       continue;

                   }

                   return number;

                }

                catch(Exception e)

                {

                   Console.WriteLine("只能输入数字,请重新输入!"+e.Message);

                }

            }

        }

 

 

 

 

        ///<summary>

        ///检测是否越界

        ///</summary>

        static voidCheckPos()

        {

            for (int i =0; i <= 1; i++)

            {

                if(playerPos[i] > 99)

                {

                   playerPos[i] = 99;

                   Console.WriteLine("玩家{i}赢得了比赛!");

                   break;

                }

                else if(playerPos[i]< 0)

                {

                   playerPos[i] = 0;

                }

            }

        }

 

    }

}

 

 

 


  • 2
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值