SDL游戏开发之三-瓦片地图

article/2025/7/21 8:12:27

一.瓦片地图

1)瓦片地图简介

瓦片地图(Tiled Map),又称为瓷砖地图,是在游戏开发中经常使用到的技术,它是由少量的尺寸相同的、小的瓦片图片拼接而成的很大的地图。相对于使用一张张图片来绘制地图而言,瓦片地图不仅大大地节省了内容,而且增加了图片的重用性和绘制性能。

使用一般的背景图会面临很多问题:

①:OpenGL ES对于纹理有大小限制,最大支持2048*2048像素,且超出这个范围则会无法显示。这个问题就可以使用瓦片地图来解决,因为瓦片地图是拼接而成的大图,从理论上说可以设置一个无限大的地图,故不存在此类问题。

②:地图中的有些位置是玩家不能进入的,比如说障碍,另外就是地图与角色之间的遮挡处理,如果使用整张的背景图,则需要额外的工作(图片和代码)来支持障碍处理和遮挡处理等问题;而使用瓦片地图的话,对于一般的碰撞能精确到瓦片级别,而对于遮挡处理也是能够做到。

瓦片地图的设计较为复杂,通常情况下,瓦片地图都是使用一些编辑器来完成的,这里使用的软件是tiled,tiled地图编辑器功能强大,除了能创建绘制层外,还可以创建对象层。对象层中包括了对象的相关数据,除了具有坐标、名称、类型等基本属性外,还可以额外添加任意的键值对,这些数据可以在代码中解析来达到特定的功能。瓦片地图主要负责代替背景图,以及处理碰撞。

2)解析及绘制原理

一般情况下,为使得文件占用体积变小,都会对tmx(tmx文件内部是xml格式)文件的绘制层的数据进行zlib压缩和base64加密;而在使用前时则要先解压,然后再base64解密,之后得到的数据即可在程序中使用(zlib是对数据进行压缩,base64是为了把压缩后的二进制数据可以通过ASCII字符串显式地表达出来)。

简单地说,瓦片地图就是从瓦片图片中找到对应的瓦片并绘制到相应的位置。

若新建一个瓦片地图,对应的瓦片图片为:

图1-瓦片图

在上图中,每个瓦片都有一个唯一的ID,左上角为1,依次递增为1、2、3...(在绘制时需要id-1,如果id-1 == 0,则不绘制)。

创建的瓦片地图的显示和对应数据如下: 

 

图2-瓦片地图

 

该图的数据如下:

37,29,29,29

29,24,25,26

45,32,33,34

29,40,41,42

 

单纯地使用瓦片地图是相对来说比较简单的,只需要解析一个外部的地图文件,或者直接写在程序里面。

瓦片地图的原理也比较简单,思路大致如下图: 

图3-瓦片地图的绘制(图片来源于网络 )

 

总地来说,瓦片地图的基本思路就是把瓦片图中的瓦片绘制到屏幕上对应的位置。

3)示例代码

//TMXTiledMap.h
/*图块集*/
struct Tileset
{int firstGirdID;int tileWidth;int tileHeight;int spacing;int margin;int width;int height;int numColumns;std::string name;
};

Tileset结构体用来保存地图文件(*.tmx)中使用到的图块的信息,其在tmx文件的结构如下:

 <tileset firstgid="1" name="blocks1" tilewidth="32" tileheight="32" spacing="2" margin="2" tilecount="198" columns="18"><image source="blocks1.png" width="614" height="376"/><tileset firstgid="199" name="blocks2" tilewidth="32" tileheight="32" spacing="2" margin="2" tilecount="198" columns="18"><image source="blocks2.png" width="614" height="376"/>

本示例的tmx文件中使用到了两个瓦片集,所以会有两个tileset标签,为了保证在一个瓦片地图中的用到的瓦片的id是唯一的,所以在tileset标签中有一个属性firstgid,它用来标识本图块的第一个瓦片的id是多少。

class TMXTiledMap
{public:TMXTiledMap(const std::string& tmxFile, SDL_Renderer* ren, int width, int height);~TMXTiledMap();void draw();private:bool initWithFile( const std::string& filepath);//解析layer中的datavoid parseTileLayer(TiXmlElement*pRoot);//解析tilesetvoid parseTilesets(TiXmlElement*pTilesetRoot);//根据id获得相应的图块集Tileset* getTilesetByID(unsigned int tileID);//根据坐标获取对应的gidint getTileGIDAt(int tileCoordinateX, int tileCoordinateY);//内部的draw,封装了SDL_RenderCopyExvoid drawTile(std::string id,int margin,int spacing,int x,int y,int width,int height,int currentRow,int currentFame);

TMXTiledMap类除了构造函数和析构函数外,目前仅有一个公有函数draw函数,它的作用就是绘制地图,其他的私有函数则是解析文件和辅助地图的绘制。

private://保存图块集std::vector<Tileset*> _tilesets;//保存地图数据std::vector<unsigned> _data;//tmx文件的宽/高瓦片个数int _mapRowTileNum;int _mapColTileNum;//tileset sizeint _tileSize;//save picturestd::map<std::string,SDL_Texture*> _textures;//rendererSDL_Renderer*_pRenderer;//可视大小,可以认为是屏幕大小int _visibleWidth;int _visibleHeight;

一维数组_data用于保存地图数据,目前仅仅用到了一个_data,因此当前的TMXTiledMap类不支持多个图层(扩展也比较简单,可以新建一个TMXLayer类,每个TMXLayer对象保存着本层的地图数据);一维数组可以用来保存二维数据,只不过在存取数据时需要一个映射(也可以使用二维数组)。

_tileSize为整型,其实瓦片的宽和高是可以不同的,不过这里为方便而使用了_tileSize同时表示瓦片的宽度和高度。

//TMXTiled.cpp
TMXTiledMap::TMXTiledMap(const std::string& tmxPath,SDL_Renderer*ren, int width, int height):_mapRowTileNum(0),_mapColTileNum(0),_tileSize(0),_pRenderer(ren),_visibleWidth(width),_visibleHeight(height)
{bool ret = this->initWithFile(txtPath);
}

构造函数除了初始化变量外,还会调用initWithFile函数来读取tmx文件并把数据赋给对应的变量。

bool TMXTiledMap::initWithFile(const std::string& filepath)
{TiXmlDocument doc;//加载tmxif(!doc.LoadFile(filepath.c_str())){std::cout<<"error:"<<doc.ErrorDesc()<<std::endl;return false;}//获得根节点TiXmlElement*pRoot = doc.RootElement();pRoot->Attribute("width",&_mapRowTileNum);pRoot->Attribute("height",&_mapColTileNum);pRoot->Attribute("tilewidth",&_tileSize);//parse the tilesetsfor(TiXmlElement*e = pRoot->FirstChildElement();e != NULL;e = e->NextSiblingElement()){std::string value = e->Value();if(value == "tileset"){parseTilesets(e);}else if (value == "layer"){this->parseTileLayer(e);}}return true;
}

tmx文件内部是xml格式,而解析tmx文件采用了第三方库tinyxml。需要注意的是,本次示例代码使用tinyxml自带的功能来加载文件,这样的使用不便于移植,若想要移植的话建议使用SDL_RWread等函数

本示例的tmx文件内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<map version="1.2" tiledversion="1.2.3" orientation="orthogonal" renderorder="right-down" width="30" height="20" tilewidth="32" tileheight="32" infinite="0" nextlayerid="4" nextobjectid="1"><tileset firstgid="1" name="blocks1" tilewidth="32" tileheight="32" spacing="2" margin="2" tilecount="198" columns="18"><image source="blocks1.png" width="614" height="376"/></tileset><tileset firstgid="199" name="blocks2" tilewidth="32" tileheight="32" spacing="2" margin="2" tilecount="198" columns="18"><image source="blocks2.png" width="614" height="376"/></tileset><layer id="3" name="块层 1" width="30" height="20"><data encoding="base64" compression="zlib">eJzN08lKxEAQgOEOit68Org8hcsbuLyBK+6CO6LeZiau49WDD+Hu0eVF1Js7qHhxBcGDfzBCKNLdyWCLBR+hu6u7K1TSo5TqRZ8qPs4s42KizlOq3jPnPFjGtjjAIY5wbMkdxBCG8SnW5NhVVHjmcRDROpPEBjaxhW3sYBd72NfsSdvffgyIuaylt3GRtr9J4uc7a0CjpiYX/W3irma0oFVzb7S/V7jGjeHMeSxgEUtYxgoKkZw2zmxHBzrRhW5DL17xhnfrG5ljlDvGMI6JhL0vt+Sd4wKXmvVb9f2d5ZCHj48U987ynIup4QnPeInZa/ufKjkv4+nzg5wC66thzglOw9wS5kpFPcF5WZ455OH/kjvcI4MqVKMGtSgL79RZE/VMhnwxlnNyzWRdjB9T7HVpRjM//Q9qc/l+I47uS3KurGnKUutf+gLLXZ7t</data></layer>
</map

initWithFile函数的功能就是依次读取上面的标签的属性和值,并赋给对应的变量(这里的layer标签就是地图的图层,因为代码原因,目前只能有一个图层)。

initWithFile在解析标签时,碰到map的子节点tileset标签和layer标签时,会分别调用parseTileset()和parseTileLayer()函数。

void TMXTiledMap::parseTilesets(TiXmlElement*pTilesetRoot)
{//create a tileset objectTileset* tileset = new Tileset();pTilesetRoot->FirstChildElement()->Attribute("width",&tileset->width);pTilesetRoot->FirstChildElement()->Attribute("height",&tileset->height);pTilesetRoot->Attribute("firstgid",&tileset->firstGirdID);pTilesetRoot->Attribute("tilewidth",&tileset->tileWidth);pTilesetRoot->Attribute("tileheight",&tileset->tileHeight);pTilesetRoot->Attribute("spacing",&tileset->spacing);pTilesetRoot->Attribute("margin",&tileset->margin);tileset->name = pTilesetRoot->Attribute("name");tileset->numColumns = tileset->width / (tileset->tileWidth + tileset->spacing);_tilesets.push_back(tileset);//TODO:load texture在这里需要注意路径的修改std::string filePath = std::string("assets/") + pTilesetRoot->FirstChildElement()->Attribute("source");SDL_Texture*tex = IMG_LoadTexture(_pRenderer,filePath.c_str());_textures[tileset->name] = tex;
}

在parseTileset()中,除了获取用到的值之外,还会加载对应的瓦片图。注意这里的文件路径为当前路径下的assets文件夹下,可根据需要自行修改。

void TMXTiledMap::parseTileLayer(TiXmlElement*pTilesetRoot)
{std::string decodedIDs;TiXmlElement*pDataNode = nullptr;for(TiXmlElement*e = pTilesetRoot->FirstChildElement();e != NULL;e = e->NextSiblingElement()){if(e->Value() == std::string("data")){pDataNode = e;}}for(TiXmlNode*e = pDataNode->FirstChild(); e != NULL; e = e->NextSibling()){//解码数据,并用string保存TiXmlText*text = e->ToText();std::string t = text->Value();decodedIDs = base64_decode(t);}//uncompress解压uLongf numGids = _mapRowTileNum * _mapColTileNum * sizeof(int);std::vector<unsigned> gids(numGids);uncompress((Bytef*)&gids[0],&numGids,(const Bytef*)decodedIDs.c_str(),decodedIDs.size());std::vector<int> layerRow(_mapRowTileNum);//_data = gids;_data = std::vector<unsigned>(_mapRowTileNum * _mapColTileNum);//拷贝数组std::copy(gids.begin(), gids.begin() + _mapRowTileNum * _mapColTileNum, _data.begin());
}

parseTileLayer会获取到<layer>的子标签<data>中的数据,先对这些数据进行解压缩,然后再使用base64进行解密,最后把得到的数据赋给_data变量。

现在默认认为采用了zlib压缩和base64加密,如果tmx文件未压缩和加密的话,则可以省略掉上述的解压和解密过程

Tileset* TMXTiledMap::getTilesetByID(unsigned int tileID)
{for(unsigned int i = 0;i < _tilesets.size();i++){/*这里的判断是如果tileID不在前m_tilesets.size()-1里面,就必定是最后一个*/if(i + 1 <= _tilesets.size() - 1){if(tileID >= _tilesets[i]->firstGirdID && tileID < _tilesets[i + 1]->firstGirdID){return _tilesets[i];}}else{return _tilesets[i];}}std::cout<<"did not find tileset,rerturning empty tileset\n";return nullptr;
}

getTilesetByID()函数会根据瓦片的id来获取它所对应的Tileset对象,如果未找到则返回空指针。

int TMXTiledMap::getTileGIDAt(int tileCoordinateX, int tileCoordinateY)
{if (tileCoordinateX >= 0 && tileCoordinateX < _mapRowTileNum &&tileCoordinateY >= 0 && tileCoordinateY < _mapColTileNum){int z = (int)(tileCoordinateX + tileCoordinateY * _mapRowTileNum);return _data[z];}return 0;

getTileGIDAt函数会根据提供的图块坐标来获取所对应的瓦片id。

在瓦片地图中,一般包含两种坐标:第一种是像素级坐标,这个是较为常用的坐标,无论是绘制地图,还是逻辑处理等一般都是使用的此类坐标;第二类则是图块坐标图块坐标是以瓦片的大小为一个单位的坐标。

以上两种坐标是可以相互转换的,像素坐标除以瓦片尺寸就会得到瓦片坐标(整型的除法)。

void TMXTiledMap::drawTile(std::string name,int margin,int spacing,int x,int y,int width,int height,int currentRow,int currentFrame)
{SDL_Rect srcRect;SDL_Rect destRect;srcRect.x = margin + (spacing + width) * currentFrame;srcRect.y = margin + (spacing + height) * currentRow;srcRect.w = destRect.w = width;srcRect.h = destRect.h = height;destRect.x = x;destRect.y = y;SDL_RenderCopyEx(_pRenderer, _textures[name], &srcRect, &destRect, 0, 0, SDL_FLIP_NONE);
}

drawTile封装了一个专门用于绘制瓦片的函数:name是用到的SDL_Texture纹理名称(纹理是存储在_textures中的);marrgin和spacing是保存在Tileset中的两个属性,margin是瓦片图的外边距,而spacing则是瓦片图的内边距;currentRow、currentFrame、width和height用来控制从某行某列的尺寸为(width, height)的矩形绘制到(x,y,width,height)上,即SDL中的srcRect;x、y、width、height用来控制绘制到哪里,即SDL中的destRect

void TMXTiledMap::draw()
{//对偏移位置进行取反SDL_Point pos = { 0, 0 };//从哪开始绘制int startX = pos.x / _tileSize;int startY = pos.y / _tileSize;//绘制到哪 多绘制一个int endX = startX + _visibleWidth / _tileSize + 1;int endY = startY + _visibleHeight / _tileSize + 1;endX = endX > _mapRowTileNum ? _mapRowTileNum : endX;endY = endY > _mapColTileNum ? _mapColTileNum : endY;//只绘制屏幕for(int i = startY;i < endY;i++){for(int j = startX;j < endX;j++){int id = this->getTileGIDAt(j, i);//0代表无图块if(id == 0){continue;}Tileset* tileset = getTilesetByID(id);id--;drawTile(tileset->name,tileset->margin,tileset->spacing,j * _tileSize,i * _tileSize,_tileSize,_tileSize,(id - (tileset->firstGirdID - 1))/tileset->numColumns,(id - (tileset->firstGirdID - 1))%tileset->numColumns);}}
}

draw函数用于完全绘制。

接着则是主函数:

#include<iostream>#include "SDL.h"
#include "TMXTiledMap.h"using namespace std;
//全局常量
const int FPS = 60;
const int DELAY_TIME = 1000/FPS;
//窗口和渲染器
SDL_Window* gWin = nullptr;
SDL_Renderer* gRen = nullptr;bool init();
SDL_Point getScroll(SDL_Keycode keycode);int main(int argc,char** argv)
{//地图显示TMXTiledMap* pTiledMap = nullptr;//Uint32 frameStart = 0, frameTime = 0;SDL_Event event;bool running = true;SDL_Keycode keycode = SDLK_UNKNOWN;//初始化SDL环境if (init()){cout << "初始化成功" << endl;}else{return -1;}pTiledMap = new TMXTiledMap("assets/map1.tmx",gRen, 640, 480);//循环while(running){frameStart = SDL_GetTicks();SDL_RenderClear(gRen);//add code here..pTiledMap->draw();SDL_RenderPresent(gRen);//update//获取事件while(SDL_PollEvent(&event)){switch (event.type){case SDL_QUIT:running = false;break;case SDL_KEYDOWN:keycode = event.key.keysym.sym;break;case SDL_KEYUP:keycode = SDLK_UNKNOWN;break;default:break;}}frameTime = SDL_GetTicks() - frameStart;if (frameTime < DELAY_TIME){SDL_Delay(int(DELAY_TIME - frameTime));}}//释放内存delete pTiledMap;SDL_DestroyRenderer(gRen);SDL_DestroyWindow(gWin);SDL_Quit();return 0;
}

主函数则比较简单,显示创建了窗口和渲染器,然后创建了一个tiledMap对象,之后开始进入游戏循环。

bool init()
{//初始化SDLif (SDL_Init(SDL_INIT_EVERYTHING) == 0){if((gWin = SDL_CreateWindow("TileMapTest", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,640, 480,SDL_WINDOW_SHOWN)) == NULL){cout<<"error:"<<SDL_GetError()<<endl;return false;}else if((gRen = SDL_CreateRenderer(gWin, -1,SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC | SDL_RENDERER_TARGETTEXTURE)) == NULL){cout<<"error:"<<SDL_GetError()<<endl;return false;}SDL_SetRenderDrawColor(gRen,210,250,255,255);}return true;
}

运行结果如下:


http://chatgpt.dhexx.cn/article/7rtsA6py.shtml

相关文章

瓦片地图是什么鬼

瓦片地图的诞生 地图数据通常体量较大&#xff0c;需要充足的带宽和数据渲染能力。瓦片地图诞生以前&#xff0c;地图多在局域网的桌面软件中使用。互联网的发展&#xff0c;催生了通过浏览器使用地图的需求&#xff0c;于是在1999年&#xff0c;出现了WMS&#xff08;Web Map …

Unity-瓦片地图详解

前言 在学习瓦片地图的使用时&#xff0c;我发现无论国内外还是Unity官方的相关教程都比较散&#xff0c;接触的比较浅&#xff0c;学的我挺难受的&#xff0c;所以就把各个地方看的教程加上我自己的理解&#xff0c;和官方的API手册&#xff0c;总结出了这个详解。 0. 瓦片地…

html2canvas页面截图图片不显示

前两天在一个群里&#xff0c;有人问使用html2canvas屏幕截图的时候为什么页面的图片不显示只显示了文字&#xff0c;我没有做过屏幕截图的需求&#xff0c;所以不是很清楚&#xff0c;今天稍稍测试了一下。 在github上将html2canvas源码下载到本地&#xff0c;examples文件夹…

js如何实现页面截图生成并分享功能,如何向后端传递

通过html2canvas生成分享图片 什么是 html2canvs? html2canvas 的作用就是允许让我们直接在用户浏览器上拍摄网页或其部分的“截图”。它的屏幕截图是基于 DOM 的&#xff0c;因此可能不会 100% 精确到真实的表示&#xff0c;因为它不会生成实际的屏幕截图&#xff0c;而是基…

移动端H5页面截图【含 domtoimage、html2canvas 】

起因&#xff1a; 由于云栖大会项目需要支持 名片保存到本地功能【如下图红框】&#xff0c;所以&#xff0c;需要单独截取页面中的名片信息&#xff0c;并且保存到本地。 推荐给&#xff1a;需要获取页面内容&#xff0c;给页面截图的小伙伴 介绍两个工具&#xff1a; html2c…

小程序 超长页面截图保存web-view+html2canvas

web-view文档建议参考----支付宝提供的文档&#xff0c;html2canvas官方文档&#xff08;官网可以下载html2canvas.js 和 html2canvas.min.js&#xff09;。由于篇幅受限&#xff0c;这里就贴了一下用法&#xff0c;对于web-view的配置情况&#xff0c;需要自己去查看文档&…

服务端实现对页面截图 - PhantomJs

版权声明 : 本文为博主原创文章,如需转载,请注明出处(https://blog.csdn.net/F1004145107/article/details/97786555) 目录 / 1 / 前言 / 2 / 使用方式 / 3 / 结语 / 1 / 前言 本文主要讲解的是关于在Java服务端使用PhandomJs来实现对指定页面(页面可包含echarts等图表)进…

js保存当前页面的截图 (leaflet下载当前页面截图,线会偏移问题解决)

这是保存的leaflet地图 上代码: html <div id="map" class="map"></div>ts代码: var mapid = document.querySelector(#map);const scale = window.devicePixelRatio;// // 传入节点原始宽高const _width = mapid["offsetWidth"]…

react单个页面截图和多个页面截图

之前有一个需求是点击导出按钮&#xff0c;然后去各个页面截图&#xff0c;将图片传给后端&#xff0c;后端返回文件流&#xff0c;导出ppt报告。这个需求听到的时候头都大了&#xff0c;不知道怎么做&#xff0c;之后有思路了&#xff0c;在实现过程中也出现了各种问题&#x…

JS页面截图

1、引入 js 【密码&#xff1a;gf33】 点击下载 html2canvas.min.js <script src"{url:static/octzz/js/html2canvas.min.js}"></script>2、html代码 <body><div><button type"button" class"layui-btn" onclick&q…

使用html2canvas.js实现页面截图

由于最近项目需求&#xff0c;需要实现html页面截图&#xff0c;经过查资料&#xff0c;找到了这个东东html2canvas&#xff0c;刚开始截出来的图片&#xff0c;空白、模糊、图片不完整&#xff0c;各种问题&#xff0c;但幸好有各路大神的见解&#xff0c;把问题解决了&#x…

vue页面截图;H5页面截图;vue项目中将特定网页内容生成图片(截图);html2canvas截图

功能&#xff1a; 1.兼容 PC 和 Mobile&#xff1b; 2.对指定的区域进行截取&#xff1b; 3.可以控制截图大小&#xff1b; 4.截图生成base64图片地址 一、安装插件 npm install html2canvas --save 或 yarn add html2canvas二、在.vue页面引入使用 import html2canvas fro…

Java实现HTML页面截图功能

概述 业务开发中&#xff0c;经常会有HTML页面截图&#xff0c;或打印另存为PDF文件的需求。本文即是HTML页面截图需求的技术调研过程的成文。不想看长篇大论的同学&#xff0c;可以直接看Selenium部分&#xff0c;本人最后也是采取此方案。 html2canvas 直接上代码&#xf…

2种方式!带你快速实现前端截图

导语 | 本文将介绍在前端开发中页面截图的两种方式&#xff0c;包括对其实现原理和使用方式进行详细阐述&#xff0c;希望能为更多前端开发者提供一些经验和帮助。 一、 背景 页面截图功能在前端开发中&#xff0c;特别是营销场景相关的需求中, 是比较常见的。比如截屏分享&…

Element el-row el-col 布局组件详解

点此查看全部文字教程、视频教程、源代码 本文目录 1. 背景2. 分栏布局3. 分栏间隔4. 分栏偏移4. 对齐方式5. 响应式布局6. 小结 1. 背景 element的布局方式与bootstrap原理是一样的&#xff0c;将网页划分成若干行&#xff0c;然后每行等分为若干列&#xff0c;基于这样的方式…

Android基础篇 屏幕横竖屏切换(layout-land)下篇

默认情况下Activity进行屏幕旋转会自动进行onDestroy并重新onCreate 一、非默认情况下 在AndroidManifest.xml的Activity中配置 <activityandroid:name".ui.activity.XXXXXActivity"android:launchMode"singleTask"android:screenOrientation"por…

Android屏幕共享解决方案

屏幕共享是增强互动体验&#xff0c;提高沟通效率的重要功能。以下是一些实现Android屏幕共享的解决方案&#xff1a; 1、使用视频通话功能&#xff1a;在Android中&#xff0c;可以使用视频通话功能实现屏幕共享。通过视频通话&#xff0c;可以将自己的屏幕内容以视频的方式分…

让div居中的方式的几种方法

让div水平居中的方式的几种方法。 文章目录 一、margin二、绝对定位三、子元素绝对定位父元素相对定位四、flex布局总结 一、margin 第一种方式我们可以利用外边距属性来使div水平垂直居中 先来看一段有问题的代码 <!DOCTYPE html> <html lang"en"> &…

html网页图片和文字水平居中垂直居中显示

关注公众号&#xff1a;”奇叔码技术“ 回复&#xff1a;“java面试题大全”或者“java面试题” 即可免费领取资料 下面代码一致&#xff1a;示例 自己做的网站效果示例 div相对于页面水平居中显示&#xff1a; 核心代码&#xff1a;margin&#xff1a;0 auto&#xff1b;…

Android 双屏异显(Presentation) 开发,将第二个页面投屏到副屏上

1. 背景 最近开发的一个项目&#xff0c;有两个屏幕&#xff0c;需要将第二个页面投屏到副屏上&#xff0c; 这就需要用到Android的双屏异显(Presentation)技术了&#xff0c;研究了一下&#xff0c;这里做下笔记。 我们那个副屏是一块汽车的后视镜(流媒体后视镜)&#xff0c;…