EasyX实现俄罗斯方块游戏

1 启动页面

1.1 启动页面分析

启动页面:
在这里插入图片描述
启动页面分析:
在这里插入图片描述
启动界面的大小为:550 X 660(像素)。

1.2 启动界面代码实现

# include <stdio.h> 
# include <graphics.h> 

void welcome(void) {
	initgraph(550, 660);

	// 设置窗口标题
	HWND hwnd = GetHWnd();
	SetWindowText(hwnd, "俄罗斯方块");
	//Sleep(2000);

	// 游戏标题
	setfont(40, 0, "微软雅黑");
	setcolor(WHITE);
	outtextxy(205, 200, "俄罗斯方块!");

	// 游戏副标题
	setfont(22, 0, "楷体");
	outtextxy(175, 300, "编程,从俄罗斯方块开始!");
	Sleep(3000);
}


int main()
{
	welcome();
	
	closegraph();
	return 0;
}

2 初始化游戏环境

2.1 界面效果及分析

效果:
在这里插入图片描述
分析:
在这里插入图片描述

2.2 代码实现

int score = 0; // 总分
int rank = 0;  //等级

void initGameScene()
{
	char str[16];
	
	cleardevice();
	setcolor(WHITE);

	rectangle(29, 29, 334, 633);
	rectangle(27, 27, 336, 635);
	rectangle(370, 50, 515, 195);

	setfont(24, 0, "楷体");
	setcolor(LIGHTGRAY);
	outtextxy(405, 215, "下一个:");
	setcolor(RED);
	outtextxy(405, 280, "分数:");
	sprintf(str, "%d", score);
	outtextxy(415, 310, str);
	outtextxy(405, 375, "等级:");
	sprintf(str, "%d", rank);
	outtextxy(425, 405, str);
	setfont(22, 0, "楷体");

	setcolor(LIGHTBLUE);
	outtextxy(390, 475, "操作说明:");
	outtextxy(390, 500, "↑: 旋转");
	outtextxy(390, 525, "↓: 下降");
	outtextxy(390, 550, "←: 左移");
	outtextxy(390, 575, "→: 右移");
	outtextxy(390, 600, "空格: 暂停");
}


int main()
{
	welcome();
	initGameScene();

	system("pause");
	closegraph();
	return 0;
}

3 新方块

3.1 显示效果

在这里插入图片描述

3.2 分析

以L型方块为例:
在这里插入图片描述
每个方块有4种形态:4个方向,所以使用4个二维数组来表示1种方块。

	{ 0,0,0,0,0,
	0,0,1,0,0,
	0,0,1,0,0,
	0,0,1,1,0,
	0,0,0,0,0 },

	{ 0,0,0,0,0,
	0,0,0,0,0,
	0,1,1,1,0,
	0,1,0,0,0,
	0,0,0,0,0 },

	{ 0,0,0,0,0,
	0,1,1,0,0,
	0,0,1,0,0,
	0,0,1,0,0,
	0,0,0,0,0 },

	{ 0,0,0,0,0,
	0,0,0,1,0,
	0,1,1,1,0,
	0,0,0,0,0,
	0,0,0,0,0 }

3.3 代码实现

#define  BLOCK_COUNT	    5
#define  BLOCK_WIDTH		5
#define  BLOCK_HEIGHT  	    5
#define  UNIT_SIZE		    20  //小砖块的宽度和高度

int color[BLOCK_COUNT] = {
	GREEN,CYAN,MAGENTA,BROWN,YELLOW
};

int NextIndex = -1;


int block[BLOCK_COUNT * 4][BLOCK_HEIGHT][BLOCK_WIDTH] = {
	// | 形方块
	{ 0,0,0,0,0,
	0,0,1,0,0,
	0,0,1,0,0,
	0,0,1,0,0,
	0,0,0,0,0 },

	{ 0,0,0,0,0,
	0,0,0,0,0,
	0,1,1,1,0,
	0,0,0,0,0,
	0,0,0,0,0 },

	{ 0,0,0,0,0,
	0,0,1,0,0,
	0,0,1,0,0,
	0,0,1,0,0,
	0,0,0,0,0 },

	{ 0,0,0,0,0,
	0,0,0,0,0,
	0,1,1,1,0,
	0,0,0,0,0,
	0,0,0,0,0 },

	// L 形方块
	{ 0,0,0,0,0,
	0,0,1,0,0,
	0,0,1,0,0,
	0,0,1,1,0,
	0,0,0,0,0 },

	{ 0,0,0,0,0,
	0,0,0,0,0,
	0,1,1,1,0,
	0,1,0,0,0,
	0,0,0,0,0 },

	{ 0,0,0,0,0,
	0,1,1,0,0,
	0,0,1,0,0,
	0,0,1,0,0,
	0,0,0,0,0 },

	{ 0,0,0,0,0,
	0,0,0,1,0,
	0,1,1,1,0,
	0,0,0,0,0,
	0,0,0,0,0 },

	// 田 形方块
	{ 0,0,0,0,0,
	0,1,1,0,0,
	0,1,1,0,0,
	0,0,0,0,0,
	0,0,0,0,0 },

	{ 0,0,0,0,0,
	0,1,1,0,0,
	0,1,1,0,0,
	0,0,0,0,0,
	0,0,0,0,0 },

	{ 0,0,0,0,0,
	0,1,1,0,0,
	0,1,1,0,0,
	0,0,0,0,0,
	0,0,0,0,0 },

	{ 0,0,0,0,0,
	0,1,1,0,0,
	0,1,1,0,0,
	0,0,0,0,0,
	0,0,0,0,0 },

	// T 形方块
	{ 0,0,0,0,0,
	0,1,1,1,0,
	0,0,1,0,0,
	0,0,0,0,0,
	0,0,0,0,0 },

	{ 0,0,0,0,0,
	0,0,0,1,0,
	0,0,1,1,0,
	0,0,0,1,0,
	0,0,0,0,0 },

	{ 0,0,0,0,0,
	0,0,1,0,0,
	0,1,1,1,0,
	0,0,0,0,0,
	0,0,0,0,0 },

	{ 0,0,0,0,0,
	0,1,0,0,0,
	0,1,1,0,0,
	0,1,0,0,0,
	0,0,0,0,0 },

	// Z 形方块
	{ 0,0,0,0,0,
	0,1,1,0,0,
	0,0,1,1,0,
	0,0,0,0,0,
	0,0,0,0,0 },

	{ 0,0,0,0,0,
	0,0,1,0,0,
	0,1,1,0,0,
	0,1,0,0,0,
	0,0,0,0,0 },

	{ 0,0,0,0,0,
	0,1,1,0,0,
	0,0,1,1,0,
	0,0,0,0,0,
	0,0,0,0,0 },

	{ 0,0,0,0,0,
	0,0,1,0,0,
	0,1,1,0,0,
	0,1,0,0,0,
	0,0,0,0,0 },
};


void clearBlock(int x, int y) {
	setcolor(BLACK);
	setfont(23, 0, "楷体");
	for (int i = 0; i<BLOCK_HEIGHT; i++)
		for (int j = 0; j<BLOCK_WIDTH; j++)
			outtextxy(x + UNIT_SIZE*j, y + UNIT_SIZE*i, "■");
}


void drawBlock(int x, int y, int index) {
	setfont(23, 0, "楷体");
	setcolor(color[index]);
	for (int i = 0; i<5; i++)
		for (int j = 0; j<5; j++)
			if (block[4 * index][i][j] == 1)
				outtextxy(x + 20 * j, y + 20 * i, "■");
}

void nextblock()
{
	int x = 391, y = 71;

	//在右侧的提示区清除原来的方块
	clearBlock(x, y);
	

	// 在右侧的提示区绘制新方块
	// 1. 产生新的方块
	srand(time(NULL));
	NextIndex = rand() % BLOCK_COUNT;
	// 2. 绘制
	drawBlock(x, y, NextIndex);
}


int main()
{
	welcome();
	initGameScene();
	nextblock();

	system("pause");
	closegraph();
	return 0;
}

4 降落方块

4.1 使用访问数组确定是否有方块

int visit[30][15], Color[30][15]; // visit[i][j] == 1 表示该位置有方块

int main()
{
	welcome();
	initGameScene();
	nextblock();

	// 清空访问数组
	Sleep(500);
	memset(visit, 0, sizeof(visit));

	system("pause");
	closegraph();
	return 0;
}

4.2 设计游戏循环

int BlockIndex = -1;  //当前方块的序号

void newblock() { //新方块下降

}

int main()
{
	welcome();
	initGameScene();
	nextblock();

	//  清空访问数组
	Sleep(500);
	memset(visit, 0, sizeof(visit));

	// 最开始时, 第一个方块,就是下一个方块
	BlockIndex = NextIndex;

	while (1)
	{
		newblock();

	}

	system("pause");
	closegraph();
	return 0;
}

5 全部代码实现

#include <stdio.h>
#include <graphics.h>
#include <time.h>
#include <conio.h> //kbhit()使用

int score = 0; //总分
int rank = 0;  //等级

#define BLOCK_COUNT    5
#define BLOCK_WIDTH    5
#define BLOCK_HEIGHT	  5
#define UNIT_SIZE          20

#define START_X   130
#define START_Y	30

#define KEY_UP			72
#define	KEY_RIGHT	77
#define KEY_DOWN	80
#define KEY_LEFT		75
#define KEY_SPACE	32

int speed = 500;
int minX = 30;
int minY = 30;

typedef enum {
	BLOCK_UP,
	BLOCK_RIGHT,
	BLOCK_DOWN,
	BLOCK_LEFT
} block_dir_t;

typedef enum {
	MOVE_DOWN,
	MOVE_LEFT,
	MOVE_RIGHT
} move_dir_t;

int NextIndex = -1;  //下一个方块的种类
int BlockIndex = -1; //当前方块的种类

int color[BLOCK_COUNT] = {
	GREEN, CYAN,  MAGENTA, BROWN, YELLOW
};

int visit[30][15]; //访问数组
int markColor[30][15]; //表示对应位置的颜色

int block[BLOCK_COUNT*4][BLOCK_HEIGHT][BLOCK_WIDTH] = {
	// |  型方块
	{
		0, 0, 0, 0, 0,
	    0, 0, 1, 0, 0,
		0, 0, 1, 0, 0,
		0, 0, 1, 0, 0,
		0, 0, 0, 0, 0 },
	{ 
		0,0,0,0,0,
	    0,0,0,0,0,
	    0,1,1,1,0,
	    0,0,0,0,0,
	    0,0,0,0,0 },
	{  0,0,0,0,0,
	    0,0,1,0,0,
     	0,0,1,0,0,
	    0,0,1,0,0,
	    0,0,0,0,0 },

	{ 0,0,0,0,0,
	   0,0,0,0,0,
	   0,1,1,1,0,
	   0,0,0,0,0,
	   0,0,0,0,0 },

	// L 形方块
	{ 0,0,0,0,0,
	0,0,1,0,0,
	0,0,1,0,0,
	0,0,1,1,0,
	0,0,0,0,0 },

	{ 0,0,0,0,0,
	0,0,0,0,0,
	0,1,1,1,0,
	0,1,0,0,0,
	0,0,0,0,0 },

	{ 0,0,0,0,0,
	0,1,1,0,0,
	0,0,1,0,0,
	0,0,1,0,0,
	0,0,0,0,0 },

	{ 0,0,0,0,0,
	0,0,0,1,0,
	0,1,1,1,0,
	0,0,0,0,0,
	0,0,0,0,0 },

	// 田 形方块
	{ 0,0,0,0,0,
	0,1,1,0,0,
	0,1,1,0,0,
	0,0,0,0,0,
	0,0,0,0,0 },

	{ 0,0,0,0,0,
	0,1,1,0,0,
	0,1,1,0,0,
	0,0,0,0,0,
	0,0,0,0,0 },

	{ 0,0,0,0,0,
	0,1,1,0,0,
	0,1,1,0,0,
	0,0,0,0,0,
	0,0,0,0,0 },

	{ 0,0,0,0,0,
	0,1,1,0,0,
	0,1,1,0,0,
	0,0,0,0,0,
	0,0,0,0,0 },

	// T 形方块
	{ 0,0,0,0,0,
	0,1,1,1,0,
	0,0,1,0,0,
	0,0,0,0,0,
	0,0,0,0,0 },

	{ 0,0,0,0,0,
	0,0,0,1,0,
	0,0,1,1,0,
	0,0,0,1,0,
	0,0,0,0,0 },

	{ 0,0,0,0,0,
	0,0,1,0,0,
	0,1,1,1,0,
	0,0,0,0,0,
	0,0,0,0,0 },

	{ 0,0,0,0,0,
	0,1,0,0,0,
	0,1,1,0,0,
	0,1,0,0,0,
	0,0,0,0,0 },

	// Z 形方块
	{ 0,0,0,0,0,
	0,1,1,0,0,
	0,0,1,1,0,
	0,0,0,0,0,
	0,0,0,0,0 },

	{ 0,0,0,0,0,
	0,0,1,0,0,
	0,1,1,0,0,
	0,1,0,0,0,
	0,0,0,0,0 },

	{ 0,0,0,0,0,
	0,1,1,0,0,
	0,0,1,1,0,
	0,0,0,0,0,
	0,0,0,0,0 },

	{ 0,0,0,0,0,
	0,0,1,0,0,
	0,1,1,0,0,
	0,1,0,0,0,
	0,0,0,0,0 }
};

// 欢迎界面
void welcome(void) {
	// 初始化画布
	initgraph(550, 660);

	// 设置窗口标题
	HWND window = GetHWnd(); //获取窗口
	SetWindowText(window, _T("俄罗斯方块    奇牛学院 Rock")); //设置窗口标题

	// 设置文本的字体样式
	setfont(40, 0, _T("微软雅黑"));
	setcolor(WHITE);
	outtextxy(205, 200, _T("俄罗斯方块"));

	setfont(22, 0, _T("楷体"));
	outtextxy(175, 300, _T("编程, 从俄罗斯方块开始!"));

	Sleep(3000); //睡眠(暂停)3000毫秒,3秒针
}

// 初始化游戏场景
void initGameScene(void) {
	char str[16];

	//清除屏幕
	cleardevice();

	rectangle(27, 27, 336, 635);
	rectangle(29, 29, 334, 633);
	rectangle(370, 50, 515, 195);

	setfont(24, 0, _T("楷体"));
	setcolor(LIGHTGRAY);
	outtextxy(405, 215, _T("下一个"));

	setcolor(RED);
	outtextxy(405, 280, _T("分数"));
	sprintf(str, "%d", score); 
	outtextxy(415, 310, str);

	outtextxy(405, 375, _T("等级"));
	sprintf(str, "%d", rank); 
	outtextxy(425, 405, str);

	// 操作说明  ↑  ↓ ← →
	setcolor(LIGHTBLUE);
	outtextxy(390, 475, "操作说明");
	outtextxy(390, 500, "↑:旋转");
	outtextxy(390, 525, "↓: 下降");
	outtextxy(390, 550, "←: 左移");
	outtextxy(390, 575, "→: 右移");
	outtextxy(390, 600, "空格:暂停");
}

void clearBlock(void) {
	setcolor(BLACK);
	setfont(23, 0, "楷体");

	for (int i=0; i<BLOCK_HEIGHT; i++) {
		for (int j=0; j<BLOCK_WIDTH; j++) {
			//"■"
			int x = 391 + j * UNIT_SIZE;
			int y = 71 + i * UNIT_SIZE;
			outtextxy(x, y, "■");
		}
	}
}






// 绘制方块
void drawBlock(int x, int y) {
	setcolor(color[NextIndex]);
	setfont(23, 0, "楷体");

	for (int i=0; i<BLOCK_HEIGHT; i++) {
		for (int j=0; j<BLOCK_WIDTH; j++) {
			//"■"
			if (block[NextIndex*4][i][j] == 1) {
				int x2 = x + j * UNIT_SIZE;
				int y2 = y + i * UNIT_SIZE;
				outtextxy(x2, y2, "■");
			}
		}
	}
}

// 绘制方块:  在指定位置绘制指定方块的指定方向
void drawBlock(int x, int y, int blockIndex, block_dir_t dir) {
	setcolor(color[blockIndex]);
	setfont(23, 0, "楷体");
	int id = blockIndex * 4 + dir;

	for (int i=0; i<BLOCK_HEIGHT; i++) {
		for (int j=0; j<BLOCK_WIDTH; j++) {
			//"■"
			if (block[id][i][j] == 1) {
				int x2 = x + j * UNIT_SIZE;
				int y2 = y + i * UNIT_SIZE;
				outtextxy(x2, y2, "■");
			}
		}
	}
}



// 清除指定位置指定方向的方块
// 参数x: 方块的左上角的x坐标
// 参数y: 方块的左上角在游戏区域内的坐标,距离游戏区域顶部的距离
void clearBlock(int x, int y, block_dir_t dir) {
	setcolor(BLACK);
	int id = BlockIndex * 4 + dir;
	y += START_Y;

	for (int i=0; i<5; i++) {
		for (int j=0; j<5; j++) {
			if (block[id][i][j] == 1) {
				// 擦除该方块的第i行的第j列
				outtextxy(x+20*j,  y+i*20, "■");
			}
		}
	}
}

void nextblock(void) {
	clearBlock(); // 清除右上角区域

	// 随机选择一种方块
	srand(time(NULL)); //使用时间函数的返回值,来作为随机种子
    NextIndex =	rand() % BLOCK_COUNT;

	drawBlock(391, 71);
}

// 如果在指定位置可以向指定方向移动,就返回1, 否则就返回0
int moveable(int x0, int y0, move_dir_t moveDir, block_dir_t blockDir) {
	// 计算当前方块的左上角在30x15的游戏区中的位置(第多少行,第多少列)
	int x = (y0 - minY) / UNIT_SIZE;
	int y = (x0 - minX) / UNIT_SIZE;
	int id = BlockIndex * 4 + blockDir;
	int ret = 1;

	if (moveDir == MOVE_DOWN) {
		for (int i=0; i<5; i++) {
			for (int j=0; j<5; j++) {
				if (block[id][i][j] == 1 &&
					(x + i + 1 >= 30  ||  visit[x+i+1][y+j] == 1)) {
						 ret = 0;
				}
			}
		}
	} else if (moveDir == MOVE_LEFT) {
		for (int i=0; i<5; i++) {
			for (int j=0; j<5; j++) {
				if (block[id][i][j] == 1 &&
					(y + j == 0 ||  visit[x+i][y+j-1]==1)) {
						ret = 0;
				}
			}
		}

	} else if (moveDir == MOVE_RIGHT) {
		for (int i=0; i<5; i++) {
			for (int j=0; j<5; j++) {
				if (block[id][i][j] == 1 && 
					(y+j+1>=15 || visit[x+i][y+j+1]==1)) {
						 ret = 0;
				}
			}
		}
	}

	return ret;
}

// 检测游戏是否结束
void failCheck() {
	if (!moveable(START_X, START_Y, MOVE_DOWN, BLOCK_UP)) {
		setcolor(WHITE);
		setfont(45, 0, "隶体");
		outtextxy(75, 300, "GAME OVER!");
		Sleep(1000);
		system("pause");
		closegraph();
		exit(0);
	}
}



// 判断当前方块是否可以转向到指定方向
// 注意, 此时还没有转到该方向!!!
int rotatable(int x, int y, block_dir_t dir) {
	int id = BlockIndex * 4 + dir;
	int xIndex = (y - minY) / 20;
	int yIndex = (x - minX) / 20;


	if (!moveable(x, y, MOVE_DOWN, dir)) {
		return 0;
	}

	for (int i=0; i<5; i++) {
		for (int j=0; j<5; j++) {
			 if (block[id][i][j] == 1 &&
				 (yIndex+j<0 || yIndex+j>=15 || visit[xIndex+i][yIndex+j]==1)) {
					 return 0;
			 }
		}
	}

	return 1;
}

void wait(int interval) { 
	int count  = interval / 10;
	for (int i=0; i<count; i++) {
		Sleep(10);
		if (kbhit()) {
			return;
		}
	}
}

void mark(int x, int y, int blockIndex, block_dir_t dir) {
	int id = blockIndex * 4 + dir;
	int x2 = (y - minY) / 20;
	int y2 = (x - minX) / 20;

	for (int i=0; i<5; i++) {
		for (int j=0; j<5; j++) {
			if (block[id][i][j] == 1) {
				visit[x2+i][y2+j] = 1;
				markColor[x2+i][y2+j] = color[blockIndex];
			}
		}
	}
}

void move(void){
	int x = START_X;
	int y = START_Y;
	int k = 0;
	block_dir_t  blockDir = BLOCK_UP;
	int curSpeed = speed;

	// 检测游戏是否结束
	failCheck();

	// 持续向下降落
	while (1) {
		if (kbhit()) {
			int key = getch();
			if (key == KEY_SPACE) {
				getch();
			}
		}

		// 清除当前方块
		clearBlock(x, k, blockDir);

		if (kbhit()) {
			int key = getch();

			if(key == KEY_UP) {
				block_dir_t nextDir = (block_dir_t)((blockDir + 1) % 4);
				if (rotatable(x, y+k, nextDir)) {
					blockDir = nextDir;
				}
			} else if (key == KEY_DOWN) {
				curSpeed = 50;
			} else if (key == KEY_LEFT) {
				if (moveable(x, y+k+20, MOVE_LEFT, blockDir)) {
					x -= 20;
				}
			} else if (key ==KEY_RIGHT) {
				if (moveable(x, y+k+20, MOVE_RIGHT, blockDir)) {
					x += 20;  //x = x + 20;
				}
			}
		}

		k += 20;

		// 绘制当前方块
		drawBlock(x, y+k, BlockIndex, blockDir);

		wait(curSpeed);
		
		//k += 20;

		// 方块的“固化”处理
		if (!moveable(x, y+k, MOVE_DOWN, blockDir)) {
			 mark(x, y+k, BlockIndex, blockDir);
			 break;
		}
	}
}

void newblock() {
	// 确定即将使用的方块的类别
	BlockIndex = NextIndex;

	// 绘制刚从顶部下降的方块
	drawBlock(START_X, START_Y);

	// 让新出现的方块暂停一会,让用户识别到
	Sleep(100); //0.1秒

	// 在右上角区域,绘制下一个方块
	nextblock();

	// 方块降落
	move();
}

//消除第x行,并把上面的行都下移
void down(int x){
	for (int i=x; i>0; i--) {
		// 消除第i行,第j列的方格消除
		for (int j=0; j<15; j++) {
			if (visit[i-1][j]) {
				visit[i][j] = 1;
				markColor[i][j] = markColor[i-1][j];
				setcolor(markColor[i][j]);
				outtextxy(20*j + minX, 20*i+minY, "■");
			} else {
				visit[i][j] = 0;
				setcolor(BLACK);
				outtextxy(20*j + minX, 20*i+minY, "■");
			}
		}
	}

	// 清除最顶上的哪一行(就是行标为0的那一行)
	setcolor(BLACK);
	for (int j=0; j<15; j++) {
		visit[0][j] = 0;
		outtextxy(20*j + minX, minY, "■");
	}
}

// 更新分数,参数lines表示消除的行数
void addScore(int lines) {
	char str[32];

	setcolor(RED);
	score += lines * 10;
	sprintf(str, "%d", score);
	outtextxy(415, 310, str);
}

void updateGrade() {
	// 更新等级的提示
	// 假设:50分一级
	rank = score / 50;
	char str[16];
	sprintf(str, "%d", rank);
	outtextxy(425, 405, str);

	// 更新速度, 等级越高,速度越快,speed越小!
	// 最慢:500, 最快是100
	speed = 500 - rank*100;
	if (speed <= 100) {
		speed = 100;
	}
}

void check(void) {
	int i, j;
	int clearLines = 0;

	for (i=29; i>=0; i--) {
		// 检查第i行有没有满
		for (j=0; j<15 && visit[i][j]; j++) ;

		//执行到此处时,有两种情况:
		// 1. 第i行没有满,即表示有空位 此时 j<15
		// 2. 第i行已满了,此时 j>=15
		if (j >= 15) {
			// 此时,第i行已经满了,就需要消除第i行
			down(i);  //消除第i行,并把上面的行都下移
			i++;  // 因为最外层的循环中有 i--, 所以我们先i++, 使得下次循环时,再把这一行检查一下
			clearLines++;
		}
	}

	// 更新分数
	addScore(clearLines);

	// 更新等级(更新等级提示,更新速度)
	updateGrade();
}

int main(void) {
	welcome();
	initGameScene();

	// 产生新方块
	nextblock();
	Sleep(500);

	// 初始化访问数组
	memset(visit, 0, sizeof(visit));

	while (1) {
		newblock();

		// 消除满行,并更新分数和速度
		check();
	}

	system("pause");
	closegraph();
	return 0;
}


参考资料:

  1. C/C++从入门到精通-高级程序员之路【奇牛学院】
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页