【贪吃蛇—Java程序员写Android游戏】系列 1.Android SDK Sample-Snake详解

<!-- [if gte mso 9]><xml> <w:WordDocument> <w:View>Normal</w:View> <w:Zoom>0</w:Zoom> <w:PunctuationKerning/> <w:ValidateAgainstSchemas/> <w:SaveIfXMLInvalid>false</w:SaveIfXMLInvalid> <w:IgnoreMixedContent>false</w:IgnoreMixedContent> <w:AlwaysShowPlaceholderText>false</w:AlwaysShowPlaceholderText> <w:Compatibility> <w:BreakWrappedTables/> <w:SnapToGridInCell/> <w:WrapTextWithPunct/> <w:UseAsianBreakRules/> <w:DontGrowAutofit/> <w:UseFELayout/> </w:Compatibility> <w:BrowserLevel>MicrosoftInternetExplorer4</w:BrowserLevel> </w:WordDocument> </xml><![endif]--><!-- [if gte mso 9]><xml> <w:LatentStyles DefLockedState="false" LatentStyleCount="156"> </w:LatentStyles> </xml><![endif]--><!-- [if gte mso 10]> <mce:style><!-- /* Style Definitions */ table.MsoNormalTable {mso-style-name:"Table Normal"; mso-tstyle-rowband-size:0; mso-tstyle-colband-size:0; mso-style-noshow:yes; mso-style-parent:""; mso-padding-alt:0cm 5.4pt 0cm 5.4pt; mso-para-margin:0cm; mso-para-margin-bottom:.0001pt; mso-pagination:widow-orphan; font-size:10.0pt; font-family:"Times New Roman"; mso-fareast-font-family:"Times New Roman"; mso-ansi-language:#0400; mso-fareast-language:#0400; mso-bidi-language:#0400;} --> <!-- [endif]---->

Snake也是一个经典游戏了, Nokia蓝屏机的王牌游戏之一。 Android SDK 1.5就有了它的身影。我们这里就来详细解析一下 Android SDK Sample中的 Snake工程。本工程基于 SDK 2.3.3版本中的工程,路径为: %Android_SDK_HOME% /samples/android-10/Snake

一、 Eclipse 工程

通过 File-New Project-Android-Android Project,选择“ Create project from existing sample”创建自己的应用 SnakeAndroid,如下图:

<!-- [if gte mso 9]><xml> <w:WordDocument> <w:View>Normal</w:View> <w:Zoom>0</w:Zoom> <w:PunctuationKerning/> <w:ValidateAgainstSchemas/> <w:SaveIfXMLInvalid>false</w:SaveIfXMLInvalid> <w:IgnoreMixedContent>false</w:IgnoreMixedContent> <w:AlwaysShowPlaceholderText>false</w:AlwaysShowPlaceholderText> <w:Compatibility> <w:BreakWrappedTables/> <w:SnapToGridInCell/> <w:WrapTextWithPunct/> <w:UseAsianBreakRules/> <w:DontGrowAutofit/> <w:UseFELayout/> </w:Compatibility> <w:BrowserLevel>MicrosoftInternetExplorer4</w:BrowserLevel> </w:WordDocument> </xml><![endif]--><!-- [if gte mso 9]><xml> <w:LatentStyles DefLockedState="false" LatentStyleCount="156"> </w:LatentStyles> </xml><![endif]--><!-- [if gte mso 10]> <mce:style><!-- /* Style Definitions */ table.MsoNormalTable {mso-style-name:"Table Normal"; mso-tstyle-rowband-size:0; mso-tstyle-colband-size:0; mso-style-noshow:yes; mso-style-parent:""; mso-padding-alt:0cm 5.4pt 0cm 5.4pt; mso-para-margin:0cm; mso-para-margin-bottom:.0001pt; mso-pagination:widow-orphan; font-size:10.0pt; font-family:"Times New Roman"; mso-fareast-font-family:"Times New Roman"; mso-ansi-language:#0400; mso-fareast-language:#0400; mso-bidi-language:#0400;} --> <!-- [endif]---->

运行效果如下图:

<!-- [if gte mso 9]><xml> <w:WordDocument> <w:View>Normal</w:View> <w:Zoom>0</w:Zoom> <w:PunctuationKerning/> <w:ValidateAgainstSchemas/> <w:SaveIfXMLInvalid>false</w:SaveIfXMLInvalid> <w:IgnoreMixedContent>false</w:IgnoreMixedContent> <w:AlwaysShowPlaceholderText>false</w:AlwaysShowPlaceholderText> <w:Compatibility> <w:BreakWrappedTables/> <w:SnapToGridInCell/> <w:WrapTextWithPunct/> <w:UseAsianBreakRules/> <w:DontGrowAutofit/> <w:UseFELayout/> </w:Compatibility> <w:BrowserLevel>MicrosoftInternetExplorer4</w:BrowserLevel> </w:WordDocument> </xml><![endif]--><!-- [if gte mso 9]><xml> <w:LatentStyles DefLockedState="false" LatentStyleCount="156"> </w:LatentStyles> </xml><![endif]--><!-- [if gte mso 10]> <mce:style><!-- /* Style Definitions */ table.MsoNormalTable {mso-style-name:"Table Normal"; mso-tstyle-rowband-size:0; mso-tstyle-colband-size:0; mso-style-noshow:yes; mso-style-parent:""; mso-padding-alt:0cm 5.4pt 0cm 5.4pt; mso-para-margin:0cm; mso-para-margin-bottom:.0001pt; mso-pagination:widow-orphan; font-size:10.0pt; font-family:"Times New Roman"; mso-fareast-font-family:"Times New Roman"; mso-ansi-language:#0400; mso-fareast-language:#0400; mso-bidi-language:#0400;} --> <!-- [endif]---->

二、工程结构和类图

其实 Snake的工程蛮简单的,源文件就三个: Snake.java SnakeView.java TileView.java Snake类是这个游戏的入口点, TitleView类进行游戏的绘画, SnakeView类则是对游戏控制操作的处理。 Coordinate RefreshHandler 2个辅助类,也是 SnakeView类中的内部类。其中, Coordinate是一个点的坐标( x y), RefreshHandler RefreshHandler对象绑定某个线程并给它发送消息。如下图:

<!-- [if gte mso 9]><xml> <w:WordDocument> <w:View>Normal</w:View> <w:Zoom>0</w:Zoom> <w:PunctuationKerning/> <w:ValidateAgainstSchemas/> <w:SaveIfXMLInvalid>false</w:SaveIfXMLInvalid> <w:IgnoreMixedContent>false</w:IgnoreMixedContent> <w:AlwaysShowPlaceholderText>false</w:AlwaysShowPlaceholderText> <w:Compatibility> <w:BreakWrappedTables/> <w:SnapToGridInCell/> <w:WrapTextWithPunct/> <w:UseAsianBreakRules/> <w:DontGrowAutofit/> <w:UseFELayout/> </w:Compatibility> <w:BrowserLevel>MicrosoftInternetExplorer4</w:BrowserLevel> </w:WordDocument> </xml><![endif]--><!-- [if gte mso 9]><xml> <w:LatentStyles DefLockedState="false" LatentStyleCount="156"> </w:LatentStyles> </xml><![endif]--><!-- [if gte mso 10]> <mce:style><!-- /* Style Definitions */ table.MsoNormalTable {mso-style-name:"Table Normal"; mso-tstyle-rowband-size:0; mso-tstyle-colband-size:0; mso-style-noshow:yes; mso-style-parent:""; mso-padding-alt:0cm 5.4pt 0cm 5.4pt; mso-para-margin:0cm; mso-para-margin-bottom:.0001pt; mso-pagination:widow-orphan; font-size:10.0pt; font-family:"Times New Roman"; mso-fareast-font-family:"Times New Roman"; mso-ansi-language:#0400; mso-fareast-language:#0400; mso-bidi-language:#0400;} --> <!-- [endif]---->

任何游戏都需要有个引擎来推动游戏的运行,最简化的游戏引擎就是:在一个线程中 While循环,检测用户操作,对用户的操作作出反应,更新游戏的界面,直到用户退出游戏。

Snake这个游戏中,辅助类 RefreshHandler继承自 Handler,用来把 RefreshHandler与当前线程进行绑定,从而可以直接给线程发送消息并处理消息。注意一点: Handle对消息的处理都是异步。 RefreshHandler Handler的基础上增加 sleep()接口,用来每隔一个时间段后给当前线程发送一个消息。 handleMessage()方法在接受消息后,根据当前的游戏状态重绘界面,运行机制如下:

运行机制

这比较类似定时器的概念,在特定的时刻发送消息,根据消息处理相应的事件。 update() sleep()间接的相互调用就构成了一个循环。这里要注意: mRedrawHandle绑定的是 Avtivity所在的线程,也就是程序的主线程;另外由于 sleep()是个异步函数,所以 update() sleep()之间的相互调用才没有构成死循环。

最后分析下游戏数据的保存机制,如下:

<!-- [if gte mso 9]><xml> <w:WordDocument> <w:View>Normal</w:View> <w:Zoom>0</w:Zoom> <w:PunctuationKerning/> <w:ValidateAgainstSchemas/> <w:SaveIfXMLInvalid>false</w:SaveIfXMLInvalid> <w:IgnoreMixedContent>false</w:IgnoreMixedContent> <w:AlwaysShowPlaceholderText>false</w:AlwaysShowPlaceholderText> <w:Compatibility> <w:BreakWrappedTables/> <w:SnapToGridInCell/> <w:WrapTextWithPunct/> <w:UseAsianBreakRules/> <w:DontGrowAutofit/> <w:UseFELayout/> </w:Compatibility> <w:BrowserLevel>MicrosoftInternetExplorer4</w:BrowserLevel> </w:WordDocument> </xml><![endif]--><!-- [if gte mso 9]><xml> <w:LatentStyles DefLockedState="false" LatentStyleCount="156"> </w:LatentStyles> </xml><![endif]--><!-- [if gte mso 10]> <mce:style><!-- /* Style Definitions */ table.MsoNormalTable {mso-style-name:"Table Normal"; mso-tstyle-rowband-size:0; mso-tstyle-colband-size:0; mso-style-noshow:yes; mso-style-parent:""; mso-padding-alt:0cm 5.4pt 0cm 5.4pt; mso-para-margin:0cm; mso-para-margin-bottom:.0001pt; mso-pagination:widow-orphan; font-size:10.0pt; font-family:"Times New Roman"; mso-ansi-language:#0400; mso-fareast-language:#0400; mso-bidi-language:#0400;} --> <!-- [endif]---->

这里考虑了 Activity的生命周期:如果用户在游戏期间离开游戏界面,游戏暂停;或者由于内存比较紧张, Android关闭游戏释放内存,那么当用户返回游戏界面的时候恢复到上次离开时的界面。

三、源码解析

详细解析下源代码,由于代码量不大,以注释的方式列出如下:

1、 Snake.java

/** * <p>Title: Snake</p> * <p>Copyright: (C) 2007 The Android Open Source Project. Licensed under the Apache License, Version 2.0 (the "License")</p> * @author Gavin 标注 */ package com.deaboway.snake; import android.app.Activity; import android.os.Bundle; import android.widget.TextView; /** * Snake: a simple game that everyone can enjoy. * * This is an implementation of the classic Game "Snake", in which you control a * serpent roaming around the garden looking for apples. Be careful, though, * because when you catch one, not only will you become longer, but you'll move * faster. Running into yourself or the walls will end the game. * */ // 贪吃蛇: 经典游戏,在一个花园中找苹果吃,吃了苹果会变长,速度变快。碰到自己和墙就挂掉。 public class Snake extends Activity { private SnakeView mSnakeView; private static String ICICLE_KEY = "snake-view"; /** * Called when Activity is first created. Turns off the title bar, sets up * the content views, and fires up the SnakeView. * */ // 在 activity 第一次创建时被调用 @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.snake_layout); mSnakeView = (SnakeView) findViewById(R.id.snake); mSnakeView.setTextView((TextView) findViewById(R.id.text)); // 检查存贮状态以确定是重新开始还是恢复状态 if (savedInstanceState == null) { // 存储状态为空,说明刚启动可以切换到准备状态 mSnakeView.setMode(SnakeView.READY); } else { // 已经保存过,那么就去恢复原有状态 Bundle map = savedInstanceState.getBundle(ICICLE_KEY); if (map != null) { // 恢复状态 mSnakeView.restoreState(map); } else { // 设置状态为暂停 mSnakeView.setMode(SnakeView.PAUSE); } } } // 暂停事件被触发时 @Override protected void onPause() { super.onPause(); // Pause the game along with the activity mSnakeView.setMode(SnakeView.PAUSE); } // 状态保存 @Override public void onSaveInstanceState(Bundle outState) { // 存储游戏状态到View里 outState.putBundle(ICICLE_KEY, mSnakeView.saveState()); } }

2、 SnakeView.java

/** * <p>Title: Snake</p> * <p>Copyright: (C) 2007 The Android Open Source Project. Licensed under the Apache License, Version 2.0 (the "License")</p> * @author Gavin 标注 */ package com.deaboway.snake; import java.util.ArrayList; import java.util.Random; import android.content.Context; import android.content.res.Resources; import android.os.Handler; import android.os.Message; import android.util.AttributeSet; import android.os.Bundle; import android.util.Log; import android.view.KeyEvent; import android.view.View; import android.widget.TextView; /** * SnakeView: implementation of a simple game of Snake * * */ public class SnakeView extends TileView { private static final String TAG = "Deaboway"; /** * Current mode of application: READY to run, RUNNING, or you have already * lost. static final ints are used instead of an enum for performance * reasons. */ // 游戏状态,默认值是准备状态 private int mMode = READY; // 游戏的四个状态 暂停 准备 运行 和 失败 public static final int PAUSE = 0; public static final int READY = 1; public static final int RUNNING = 2; public static final int LOSE = 3; // 游戏中蛇的前进方向,默认值北方 private int mDirection = NORTH; // 下一步的移动方向,默认值北方 private int mNextDirection = NORTH; // 游戏方向设定 北 南 东 西 private static final int NORTH = 1; private static final int SOUTH = 2; private static final int EAST = 3; private static final int WEST = 4; /** * Labels for the drawables that will be loaded into the TileView class */ // 三种游戏元 private static final int RED_STAR = 1; private static final int YELLOW_STAR = 2; private static final int GREEN_STAR = 3; /** * mScore: used to track the number of apples captured mMoveDelay: number of * milliseconds between snake movements. This will decrease as apples are * captured. */ // 游戏得分 private long mScore = 0; // 移动延迟 private long mMoveDelay = 600; /** * mLastMove: tracks the absolute time when the snake last moved, and is * used to determine if a move should be made based on mMoveDelay. */ // 最后一次移动时的毫秒时刻 private long mLastMove; /** * mStatusText: text shows to the user in some run states */ // 显示游戏状态的文本组件 private TextView mStatusText; /** * mSnakeTrail: a list of Coordinates that make up the snake's body * mAppleList: the secret location of the juicy apples the snake craves. */ // 蛇身数组(数组以坐标对象为元素) private ArrayList<Coordinate> mSnakeTrail = new ArrayList<Coordinate>(); // 苹果数组(数组以坐标对象为元素) private ArrayList<Coordinate> mAppleList = new ArrayList<Coordinate>(); /** * Everyone needs a little randomness in their life */ // 随机数 private static final Random RNG = new Random(); /** * Create a simple handler that we can use to cause animation to happen. We * set ourselves as a target and we can use the sleep() function to cause an * update/invalidate to occur at a later date. */ // 创建一个Refresh Handler来产生动画: 通过sleep()来实现 private RefreshHandler mRedrawHandler = new RefreshHandler(); // 一个Handler class RefreshHandler extends Handler { // 处理消息队列 @Override public void handleMessage(Message msg) { // 更新View对象 SnakeView.this.update(); // 强制重绘 SnakeView.this.invalidate(); } // 延迟发送消息 public void sleep(long delayMillis) { this.removeMessages(0); sendMessageDelayed(obtainMessage(0), delayMillis); } }; /** * Constructs a SnakeView based on inflation from XML * * @param context * @param attrs */ // 构造函数 public SnakeView(Context context, AttributeSet attrs) { super(context, attrs); // 构造时初始化 initSnakeView(); } public SnakeView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); initSnakeView(); } // 初始化 private void initSnakeView() { // 可选焦点 setFocusable(true); Resources r = this.getContext().getResources(); // 设置贴片图片数组 resetTiles(4); // 把三种图片存到Bitmap对象数组 loadTile(RED_STAR, r.getDrawable(R.drawable.redstar)); loadTile(YELLOW_STAR, r.getDrawable(R.drawable.yellowstar)); loadTile(GREEN_STAR, r.getDrawable(R.drawable.greenstar)); } // 开始新的游戏——初始化 private void initNewGame() { // 清空ArrayList列表 mSnakeTrail.clear(); mAppleList.clear(); // For now we're just going to load up a short default eastbound snake // that's just turned north // 创建蛇身 mSnakeTrail.add(new Coordinate(7, 7)); mSnakeTrail.add(new Coordinate(6, 7)); mSnakeTrail.add(new Coordinate(5, 7)); mSnakeTrail.add(new Coordinate(4, 7)); mSnakeTrail.add(new Coordinate(3, 7)); mSnakeTrail.add(new Coordinate(2, 7)); // 新的方向 :北方 mNextDirection = NORTH; // 2个随机位置的苹果 addRandomApple(); addRandomApple(); // 移动延迟 mMoveDelay = 600; // 初始得分0 mScore = 0; }

3、 TileView.java

/** * <p>Title: Snake</p> * <p>Copyright: (C) 2007 The Android Open Source Project. Licensed under the Apache License, Version 2.0 (the "License")</p> * @author Gavin 标注 */ package com.deaboway.snake; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.view.View; /** * TileView: a View-variant designed for handling arrays of "icons" or other * drawables. * */ // View 变种,用来处理 一组 贴片—— “icons”或其它可绘制的对象 public class TileView extends View { /** * Parameters controlling the size of the tiles and their range within view. * Width/Height are in pixels, and Drawables will be scaled to fit to these * dimensions. X/Y Tile Counts are the number of tiles that will be drawn. */ protected static int mTileSize; // X轴的贴片数量 protected static int mXTileCount; // Y轴的贴片数量 protected static int mYTileCount; // X偏移量 private static int mXOffset; // Y偏移量 private static int mYOffset; /** * A hash that maps integer handles specified by the subclasser to the * drawable that will be used for that reference */ // 贴片图像的图像数组 private Bitmap[] mTileArray; /** * A two-dimensional array of integers in which the number represents the * index of the tile that should be drawn at that locations */ // 保存每个贴片的索引——二维数组 private int[][] mTileGrid; // Paint对象(画笔、颜料) private final Paint mPaint = new Paint(); // 构造函数 public TileView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TileView); mTileSize = a.getInt(R.styleable.TileView_tileSize, 12); a.recycle(); } public TileView(Context context, AttributeSet attrs) { super(context, attrs); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TileView); mTileSize = a.getInt(R.styleable.TileView_tileSize, 12); a.recycle(); } /** * Rests the internal array of Bitmaps used for drawing tiles, and sets the * maximum index of tiles to be inserted * * @param tilecount */ // 设置贴片图片数组 public void resetTiles(int tilecount) { mTileArray = new Bitmap[tilecount]; } // 回调:当该View的尺寸改变时调用,在onDraw()方法调用之前就会被调用,所以用来设置一些变量的初始值 // 在视图大小改变的时候调用,比如说手机由垂直旋转为水平 @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { // 定义X轴贴片数量 mXTileCount = (int) Math.floor(w / mTileSize); mYTileCount = (int) Math.floor(h / mTileSize); // X轴偏移量 mXOffset = ((w - (mTileSize * mXTileCount)) / 2); // Y轴偏移量 mYOffset = ((h - (mTileSize * mYTileCount)) / 2); // 定义贴片的二维数组 mTileGrid = new int[mXTileCount][mYTileCount]; // 清空所有贴片 clearTiles(); } /** * Function to set the specified Drawable as the tile for a particular * integer key. * * @param key * @param tile */ // 给mTileArray这个Bitmap图片数组设置值 public void loadTile(int key, Drawable tile) { Bitmap bitmap = Bitmap.createBitmap(mTileSize, mTileSize, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); tile.setBounds(0, 0, mTileSize, mTileSize); // 把一个drawable转成一个Bitmap tile.draw(canvas); // 在数组里存入该Bitmap mTileArray[key] = bitmap; } /** * Resets all tiles to 0 (empty) * */ // 清空所有贴片 public void clearTiles() { for (int x = 0; x < mXTileCount; x++) { for (int y = 0; y < mYTileCount; y++) { // 全部设置为0 setTile(0, x, y); } } } /** * Used to indicate that a particular tile (set with loadTile and referenced * by an integer) should be drawn at the given x/y coordinates during the * next invalidate/draw cycle. * * @param tileindex * @param x * @param y */ // 给某个贴片位置设置一个状态索引 public void setTile(int tileindex, int x, int y) { mTileGrid[x][y] = tileindex; } // onDraw 在视图需要重画的时候调用,比如说使用invalidate刷新界面上的某个矩形区域 @Override public void onDraw(Canvas canvas) { super.onDraw(canvas); for (int x = 0; x < mXTileCount; x += 1) { for (int y = 0; y < mYTileCount; y += 1) { // 当索引大于零,也就是不空时 if (mTileGrid[x][y] > 0) { // mTileGrid中不为零时画此贴片 canvas.drawBitmap(mTileArray[mTileGrid[x][y]], mXOffset + x * mTileSize, mYOffset + y * mTileSize, mPaint); } } } } }

<!-- [if gte mso 9]><xml> <w:WordDocument> <w:View>Normal</w:View> <w:Zoom>0</w:Zoom> <w:PunctuationKerning/> <w:ValidateAgainstSchemas/> <w:SaveIfXMLInvalid>false</w:SaveIfXMLInvalid> <w:IgnoreMixedContent>false</w:IgnoreMixedContent> <w:AlwaysShowPlaceholderText>false</w:AlwaysShowPlaceholderText> <w:Compatibility> <w:BreakWrappedTables/> <w:SnapToGridInCell/> <w:WrapTextWithPunct/> <w:UseAsianBreakRules/> <w:DontGrowAutofit/> <w:UseFELayout/> </w:Compatibility> <w:BrowserLevel>MicrosoftInternetExplorer4</w:BrowserLevel> </w:WordDocument> </xml><![endif]--><!-- [if gte mso 9]><xml> <w:LatentStyles DefLockedState="false" LatentStyleCount="156"> </w:LatentStyles> </xml><![endif]--><!-- [if gte mso 10]> <mce:style><!-- /* Style Definitions */ table.MsoNormalTable {mso-style-name:"Table Normal"; mso-tstyle-rowband-size:0; mso-tstyle-colband-size:0; mso-style-noshow:yes; mso-style-parent:""; mso-padding-alt:0cm 5.4pt 0cm 5.4pt; mso-para-margin:0cm; mso-para-margin-bottom:.0001pt; mso-pagination:widow-orphan; font-size:10.0pt; font-family:"Times New Roman"; mso-ansi-language:#0400; mso-fareast-language:#0400; mso-bidi-language:#0400;} --> <!-- [endif]---->

四、工程文件下载

为了方便大家阅读,可以到如下地址下载工程源代码:

http://download.csdn.net/source/3145349

五、小结及下期预告:

本次详细解析了 Android SDK 自带 Sample—— Snake的结构和功能。下次将会把这个游戏移植到 J2ME平台上,并且比较 Android J2ME的区别和相通之处,让从事过 J2ME开发的朋友对 Android开发有个更加直观的认识。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值