跳到主要内容 基于 Unity 2022 LTS 与 UXR 3.0 SDK 开发 Rokid AR 消消乐游戏 | 极客日志
C# 大前端 算法
基于 Unity 2022 LTS 与 UXR 3.0 SDK 开发 Rokid AR 消消乐游戏 介绍使用 Unity 2022 LTS 和 Rokid UXR 3.0 SDK 开发 AR 消消乐游戏的实践。通过空间锚定棋盘、射线交互映射及递归消除逻辑,实现了从 2D 到 3D 空间的玩法迁移。重点讲解了场景初始化、状态管理、棋盘生成与宝石交互的核心代码实现。调试过程中解决了字体缺失、坐标深度及渲染管线配置等问题。该方案验证了轻量级 AR 游戏开发的可行性,为空间计算应用提供了低门槛的技术参考。
HadoopMan 发布于 2026/4/5 更新于 2026/4/13 2 浏览体验开场
想象一下,你正坐在办公室的工位前,稍微有些工作疲劳。你没有拿起手机,而是戴上了桌上的 Rokid AR Lite 。
随着设备启动,原本平淡无奇的办公桌面上方约一米处,突然凭空浮现出一块晶莹剔透、泛着微光的 8×8 宝石棋盘。这块棋盘并不是死板地贴在你的镜片上,而是稳稳地'锚定'在真实空间里。你稍微转动头部,能从侧面观察到这块棋盘的厚度感。
界面的左上角, 正在实时跳动;右上角则显示着剩余的 步数。每一颗宝石——红的、绿的、蓝的、紫的——都整齐地排布在虚空中的网格里。当你伸出手,利用 Rokid 的射线交互轻轻滑动其中的两颗宝石,伴随着清脆的音效和宝石碎裂的粒子感,三颗同色宝石瞬间消散,上方的宝石顺势滑落,填补了空缺。
Score
Moves
这不是科幻电影,而是一个基于 Unity 2022 LTS 与 Rokid UXR 3.0 SDK 开发的轻量级 AR 尝试。将最经典的'消消乐'玩法带入空间计算时代,虽然逻辑看似简单,但其背后的'空间映射'思路和开发细节,却非常值得每一位 AR 开发者和产品经理思考。
技术栈简介 在决定开发这个 AR 小游戏时,我们选择了以下这套'黄金组合':
Unity 2022.3 LTS (Long Term Support) :作为 Unity 的长期支持版本,它在性能优化和第三方库兼容性上达到了一个极佳的平衡点。对于 AR 开发这种对底层稳定性要求极高的场景,LTS 版本能有效避免很多莫名其妙的编辑器 Bug。
Rokid UXR 3.0 SDK :这是 Rokid 官方提供的核心开发套件。它集成了 AR 相机配置、空间定位(6DoF/3DoF)、手势交互模组以及完善的射线检测系统。选择 3.0 版本,是因为它在资源占用和 API 的易用性上做了大量减法,非常适合快速原型验证。
为什么要选这套组合? 核心原因只有两个字:效率 。消消乐这种游戏逻辑非常成熟,我们不希望把 80% 的精力浪费在配置 Android 环境或调试底层传感器数据上。Unity 2022 的成熟生态,加上 UXR 3.0 这种'开箱即用'的 SDK,能让我们在短短一两天内就完成从构思到真机运行的全过程。这种快速闭环能力,对于产品团队验证新交互形态至关重要。
思路迁移 在传统的手机屏幕上,消消乐是纯粹的 2D 体验。但在 AR 场景下,我们需要进行一次'降维打击'后的'升维重构'。
3.1 结构
棋盘(Board) :在手机上是背景图,在 AR 里则是悬浮在空间中的'逻辑面板'。它必须有明确的 3D 坐标(Z 轴),以确保用户不会感到视觉压迫。
宝石(Gem) :不再是单纯的 UI 图片,而是拥有 Collider(碰撞体)的游戏对象。它们必须能响应空间中的射线触发,而不仅仅是屏幕点击。
UI 系统 :Score 和 Moves 如果放在屏幕四个角,会导致用户眼睛频繁调焦,产生疲劳。在 AR 中,我们选择将 UI 固定在棋盘上方的左右两侧,随棋盘一起锚定在空间中。
3.2 交互映射 手机上的操作是手指在屏幕上拖拽,而 Rokid AR 眼镜支持手持终端射线或手势追踪。我们的开发思路是:将玩家的射线输入(或模拟鼠标输入)映射到 3D 空间的物理射线检测(Raycast)上。当射线击中宝石并产生滑动位移时,逻辑层计算出滑动的向量方向(上下左右),从而触发宝石交换。
核心实现思路 在实现层面,我们采用了高度解耦的设计,将场景初始化、游戏管理、棋盘逻辑、单体交互和预制体生成完全分开。这种做法的好处是,即便未来我们要把宝石从'圆球'换成'3D 恐龙',逻辑代码也几乎不需要修改。
4.1 场景搭建与一键初始化 (GameSetup) 在 AR 项目里,我们往往不想手动在 Hierarchy 面板里拖入几十个宝石。因此,我们编写了 GameSetup.cs。它就像是游戏的'发令枪',在运行瞬间自动创建相机、Canvas、文本,并根据代码生成的预制体初始化棋盘。
using UnityEngine;
using UnityEngine.UI;
public class GameSetup : MonoBehaviour
{
void Start ()
{
SetupGame();
}
void SetupGame ()
{
Camera mainCamera = Camera.main;
if (mainCamera != null )
{
mainCamera.transform.position = new Vector3(0 , 0 , -10 );
mainCamera.orthographic = true ;
mainCamera.orthographicSize = 5 ;
mainCamera.backgroundColor = new Color(0.1f , 0.1f , 0.15f );
}
GameObject canvasObj = new GameObject("Canvas" );
Canvas canvas = canvasObj.AddComponent<Canvas>();
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
canvasObj.AddComponent<CanvasScaler>();
canvasObj.AddComponent<GraphicRaycaster>();
GameObject scoreObj = new GameObject("ScoreText" );
scoreObj.transform.SetParent(canvasObj.transform);
Text scoreText = scoreObj.AddComponent<Text>();
scoreText.text = "Score: 0" ;
scoreText.fontSize = 36 ;
scoreText.color = Color.white;
scoreText.alignment = TextAnchor.UpperLeft;
scoreText.font = Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf" );
RectTransform scoreRect = scoreObj.GetComponent<RectTransform>();
scoreRect.anchorMin = new Vector2(0 , 1 );
scoreRect.anchorMax = new Vector2(0 , 1 );
scoreRect.pivot = new Vector2(0 , 1 );
scoreRect.anchoredPosition = new Vector2(20 , -20 );
scoreRect.sizeDelta = new Vector2(300 , 50 );
GameObject movesObj = new GameObject("MovesText" );
movesObj.transform.SetParent(canvasObj.transform);
Text movesText = movesObj.AddComponent<Text>();
movesText.text = "Moves: 30" ;
movesText.fontSize = 36 ;
movesText.color = Color.white;
movesText.alignment = TextAnchor.UpperRight;
movesText.font = Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf" );
RectTransform movesRect = movesObj.GetComponent<RectTransform>();
movesRect.anchorMin = new Vector2(1 , 1 );
movesRect.anchorMax = new Vector2(1 , 1 );
movesRect.pivot = new Vector2(1 , 1 );
movesRect.anchoredPosition = new Vector2(-20 , -20 );
movesRect.sizeDelta = new Vector2(300 , 50 );
GameObject gmObj = new GameObject("GameManager" );
GameManager gm = gmObj.AddComponent<GameManager>();
gm.scoreText = scoreText;
gm.movesText = movesText;
GameObject[] gemPrefabs = new GameObject[6 ];
Color[] colors = new Color[]
{
new Color(1f , 0.2f , 0.2f ),
new Color(0.2f , 0.8f , 0.2f ),
new Color(0.3f , 0.5f , 1f ),
new Color(1f , 0.9f , 0.2f ),
new Color(0.9f , 0.3f , 1f ),
new Color(1f , 0.6f , 0.2f )
};
string [] names = new string [] { "RedGem" , "GreenGem" , "BlueGem" , "YellowGem" , "PurpleGem" , "OrangeGem" };
for (int i = 0 ; i < 6 ; i++)
{
gemPrefabs[i] = GemPrefabCreator.CreateGemPrefab(colors[i], names[i]);
gemPrefabs[i].SetActive(false );
}
GameObject boardObj = new GameObject("Board" );
Board board = boardObj.AddComponent<Board>();
board.width = 8 ;
board.height = 8 ;
board.gemSpacing = 1f ;
board.gemPrefabs = gemPrefabs;
Debug.Log("消消乐游戏设置完成!" );
}
}
这段脚本的核心意义在于,它将原本复杂的编辑器操作变成了'一键式'。在 AR 开发中,我们经常需要调整棋盘在空间中的相对位置。通过修改 mainCamera.transform.position 或者棋盘的 Z 轴偏移,我们可以瞬间改变游戏的深度感。
4.2 游戏状态管理 (GameManager) GameManager 是游戏的'大脑',它不关心宝石是怎么消掉的,它只关心:现在多少分了?还剩几步?游戏结束了吗?
using UnityEngine;
using UnityEngine.UI;
public class GameManager : MonoBehaviour
{
public Text scoreText;
public Text movesText;
private int score = 0 ;
private int moves = 30 ;
void Start ()
{
UpdateUI();
}
public void AddScore (int points )
{
score += points;
UpdateUI();
}
public void UseMove ()
{
moves--;
UpdateUI();
if (moves <= 0 )
{
GameOver();
}
}
void UpdateUI ()
{
if (scoreText != null ) scoreText.text = "Score: " + score;
if (movesText != null ) movesText.text = "Moves: " + moves;
}
void GameOver ()
{
Debug.Log("Game Over! Final Score: " + score);
}
public void RestartGame ()
{
UnityEngine.SceneManagement.SceneManager.LoadScene(
UnityEngine.SceneManagement.SceneManager.GetActiveScene().name);
}
}
这里值得注意的一点是,我们在 UI 更新时进行了非空判断。在 AR 项目里,UI 的加载顺序有时会因为 SDK 的初始化而略有延迟,这种健壮性处理是必须的。
4.3 核心递归逻辑:棋盘生成与消除 (Board) 这是整个游戏最核心、也最复杂的部分。我们需要处理 8×8 的二维数组,并且要保证:
初始化无解 :开局不能直接有三连。
交换检测 :只有交换后能产生消除的操作才是合法的。
连锁反应 :一次消除后,宝石掉落,可能产生新的消除,这需要用到递归(Recursive)或者协程(Coroutine) 。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Board : MonoBehaviour
{
public int width = 8 ;
public int height = 8 ;
public float gemSpacing = 1f ;
public GameObject[] gemPrefabs;
private Gem[,] gems;
private bool isProcessing = false ;
private int movingGems = 0 ;
private GameManager gameManager;
void Start ()
{
gameManager = FindObjectOfType<GameManager>();
gems = new Gem[width, height];
SetupBoard();
}
void SetupBoard ()
{
for (int x = 0 ; x < width; x++)
{
for (int y = 0 ; y < height; y++)
{
int randomGemType = Random.Range(0 , gemPrefabs.Length);
while (HasMatchOnFill(x, y, randomGemType))
{
randomGemType = Random.Range(0 , gemPrefabs.Length);
}
CreateGem(x, y, randomGemType);
}
}
transform.position = new Vector3(-width * gemSpacing / 2f + gemSpacing / 2f , -height * gemSpacing / 2f + gemSpacing / 2f , 0 );
}
bool HasMatchOnFill (int column, int row, int gemType )
{
if (column > 1 && gems[column - 1 , row]?.gemType == gemType && gems[column - 2 , row]?.gemType == gemType) return true ;
if (row > 1 && gems[column, row - 1 ]?.gemType == gemType && gems[column, row - 2 ]?.gemType == gemType) return true ;
return false ;
}
void CreateGem (int x, int y, int gemType )
{
Vector2 position = new Vector2(x * gemSpacing, y * gemSpacing);
GameObject gem = Instantiate(gemPrefabs[gemType], position, Quaternion.identity);
gem.transform.parent = transform;
gem.SetActive(true );
Gem gemScript = gem.GetComponent<Gem>();
gemScript.SetPosition(x, y);
gemScript.gemType = gemType;
gems[x, y] = gemScript;
}
public void SwapGems (int col1, int row1, int col2, int row2 )
{
if (isProcessing) return ;
if (col2 < 0 || col2 >= width || row2 < 0 || row2 >= height) return ;
Gem gem1 = gems[col1, row1];
Gem gem2 = gems[col2, row2];
if (gem1 != null && gem2 != null )
StartCoroutine(SwapGemsCoroutine(gem1, gem2));
}
IEnumerator SwapGemsCoroutine (Gem gem1, Gem gem2 )
{
isProcessing = true ;
int tempCol = gem1.column, tempRow = gem1.row;
gem1.SetPosition(gem2.column, gem2.row);
gem2.SetPosition(tempCol, tempRow);
gems[gem1.column, gem1.row] = gem1;
gems[gem2.column, gem2.row] = gem2;
gem1.MoveTo(new Vector3(gem1.column * gemSpacing, gem1.row * gemSpacing, 0 ));
gem2.MoveTo(new Vector3(gem2.column * gemSpacing, gem2.row * gemSpacing, 0 ));
yield return new WaitForSeconds (0.3f ) ;
List<Gem> matches = FindAllMatches();
if (matches.Count == 0 )
{
gem1.SetPosition(gem2.column, gem2.row);
gem2.SetPosition(tempCol, tempRow);
gems[gem1.column, gem1.row] = gem1;
gems[gem2.column, gem2.row] = gem2;
gem1.MoveTo(new Vector3(gem1.column * gemSpacing, gem1.row * gemSpacing, 0 ));
gem2.MoveTo(new Vector3(gem2.column * gemSpacing, gem2.row * gemSpacing, 0 ));
yield return new WaitForSeconds (0.3f ) ;
}
else
{
gameManager.UseMove();
yield return StartCoroutine (ProcessMatches(matches )) ;
}
isProcessing = false ;
}
List<Gem> FindAllMatches ()
{
List<Gem> matches = new List<Gem>();
for (int y = 0 ; y < height; y++)
{
for (int x = 0 ; x < width - 2 ; x++)
{
if (gems[x, y] != null && gems[x + 1 , y] != null && gems[x + 2 , y] != null )
{
if (gems[x, y].gemType == gems[x + 1 , y].gemType && gems[x, y].gemType == gems[x + 2 , y].gemType)
{
if (!matches.Contains(gems[x, y])) matches.Add(gems[x, y]);
if (!matches.Contains(gems[x + 1 , y])) matches.Add(gems[x + 1 , y]);
if (!matches.Contains(gems[x + 2 , y])) matches.Add(gems[x + 2 , y]);
}
}
}
}
return matches;
}
IEnumerator ProcessMatches (List<Gem> matches )
{
gameManager.AddScore(matches.Count * 10 );
foreach (Gem gem in matches)
{
gems[gem.column, gem.row] = null ;
Destroy(gem.gameObject);
}
yield return new WaitForSeconds (0.3f ) ;
yield return StartCoroutine (DropGems( )) ;
yield return StartCoroutine (FillBoard( )) ;
List<Gem> newMatches = FindAllMatches();
if (newMatches.Count > 0 )
yield return StartCoroutine (ProcessMatches(newMatches )) ;
}
IEnumerator DropGems ()
{
for (int x = 0 ; x < width; x++)
{
for (int y = 0 ; y < height; y++)
{
if (gems[x, y] == null )
{
for (int yAbove = y + 1 ; yAbove < height; yAbove++)
{
if (gems[x, yAbove] != null )
{
gems[x, y] = gems[x, yAbove];
gems[x, yAbove] = null ;
gems[x, y].SetPosition(x, y);
gems[x, y].MoveTo(new Vector3(x * gemSpacing, y * gemSpacing, 0 ));
break ;
}
}
}
}
}
yield return new WaitForSeconds (0.5f ) ;
}
IEnumerator FillBoard ()
{
for (int x = 0 ; x < width; x++)
{
for (int y = 0 ; y < height; y++)
{
if (gems[x, y] == null )
{
int randomGemType = Random.Range(0 , gemPrefabs.Length);
Vector2 spawnPosition = new Vector2(x * gemSpacing, height * gemSpacing);
GameObject gem = Instantiate(gemPrefabs[randomGemType], spawnPosition, Quaternion.identity);
gem.transform.parent = transform;
Gem gemScript = gem.GetComponent<Gem>();
gemScript.SetPosition(x, y);
gemScript.gemType = randomGemType;
gemScript.MoveTo(new Vector3(x * gemSpacing, y * gemSpacing, 0 ));
gems[x, y] = gemScript;
}
}
}
yield return new WaitForSeconds (0.5f ) ;
}
public void OnGemMoveComplete ()
{
movingGems--;
}
}
在 AR 开发中,IEnumerator(协程)是极其好用的工具。因为宝石的下落和移动必须是平滑的动画,如果直接改变位置,玩家会觉得画面闪烁。通过协程配合 WaitForSeconds,我们能给大脑一个处理空间变化的缓冲区。
4.4 宝石的微观交互 (Gem) 每一颗宝石都是一个独立的个体,它需要知道自己被'摸'到了,并且知道被拖向了哪个方向。
using UnityEngine;
public class Gem : MonoBehaviour
{
public int column;
public int row;
public int gemType;
private Board board;
private Vector2 firstTouchPosition;
private Vector2 finalTouchPosition;
private bool isMoving = false ;
private Vector3 targetPosition;
public float moveSpeed = 10f ;
void Start ()
{
board = FindObjectOfType<Board>();
}
void Update ()
{
if (isMoving)
{
transform.position = Vector3.Lerp(transform.position, targetPosition, moveSpeed * Time.deltaTime);
if (Vector3.Distance(transform.position, targetPosition) < 0.01f )
{
transform.position = targetPosition;
isMoving = false ;
board.OnGemMoveComplete();
}
}
if (Input.GetMouseButtonDown(0 ))
{
Vector2 mousePos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
RaycastHit2D hit = Physics2D.Raycast(mousePos, Vector2.zero);
if (hit.collider != null && hit.collider.gameObject == gameObject)
firstTouchPosition = mousePos;
}
if (Input.GetMouseButtonUp(0 ))
{
Vector2 mousePos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
RaycastHit2D hit = Physics2D.Raycast(firstTouchPosition, Vector2.zero);
if (hit.collider != null && hit.collider.gameObject == gameObject)
{
finalTouchPosition = mousePos;
CalculateAngle();
}
}
}
void CalculateAngle ()
{
Vector2 direction = finalTouchPosition - firstTouchPosition;
if (direction.magnitude < 0.5f ) return ;
if (Mathf.Abs(direction.x) > Mathf.Abs(direction.y))
{
if (direction.x > 0 ) board.SwapGems(column, row, column + 1 , row);
else board.SwapGems(column, row, column - 1 , row);
}
else
{
if (direction.y > 0 ) board.SwapGems(column, row, column, row + 1 );
else board.SwapGems(column, row, column, row - 1 );
}
}
public void MoveTo (Vector3 newPosition )
{
targetPosition = newPosition;
isMoving = true ;
}
public void SetPosition (int col, int r )
{
column = col;
row = r;
}
}
这里有一个'产品级'的细节:我们在计算方向时,加入了 direction.magnitude < 0.5f 的判断。这是为了防止玩家轻微抖动导致的误触。在 AR 环境下,由于你是对着空气操作,射线的稳定性不如物理屏幕,这个缓冲区(Deadzone)非常重要。
4.5 运行时预制体生成 (GemPrefabCreator) 为了让项目包体尽可能小,我们没有使用精美的 3D 模型,而是通过代码在运行时绘制了一张 128x128 的带渐变的圆形贴图。这种'纯程序化'的资源生成方式,极其适合快速原型。
using UnityEngine;
public class GemPrefabCreator : MonoBehaviour
{
public static GameObject CreateGemPrefab (Color color, string name )
{
GameObject gem = new GameObject(name);
SpriteRenderer sr = gem.AddComponent<SpriteRenderer>();
Texture2D texture = new Texture2D(128 , 128 );
Color[] pixels = new Color[128 * 128 ];
Vector2 center = new Vector2(64 , 64 );
float radius = 60 ;
for (int y = 0 ; y < 128 ; y++)
{
for (int x = 0 ; x < 128 ; x++)
{
float distance = Vector2.Distance(new Vector2(x, y), center);
if (distance < radius)
pixels[y * 128 + x] = color * (1f - (distance / radius) * 0.3f );
else
pixels[y * 128 + x] = Color.clear;
}
}
texture.SetPixels(pixels);
texture.Apply();
sr.sprite = Sprite.Create(texture, new Rect(0 , 0 , 128 , 128 ), new Vector2(0.5f , 0.5f ), 128 );
gem.AddComponent<CircleCollider2D>().radius = 0.45f ;
gem.AddComponent<Gem>();
return gem;
}
}
开发过程中的小坑与经验 说实话,即便代码逻辑写对了,第一次运行项目时,大概率还是会遇到'翻车'现场。这里总结了几个我们在 Rokid AR Lite 调试时遇到的典型坑。
5.1 消失的字体:Arial.ttf 去哪了? 这是 Unity 新版本的'著名槽点'。在 Unity 2022 LTS 中,如果你像以前一样直接引用内置的 Arial.ttf,你会发现编译成 APK 后,眼镜里的文字全部变成了空白。控制台会疯狂报:ArgumentException: Arial.ttf is no longer a valid built in font。避坑指南: 现在的内置字体名字改成了 LegacyRuntime.ttf。在代码中加载时一定要写对,或者最稳妥的方法是:自己导入一个开源的中文字体库。
5.2 只有 UI 没棋盘?三步排查法 很多时候运行起来,能看到 Score 和 Moves 在跳,但眼前一片漆黑,宝石棋盘没了。这时候不要慌,按以下顺序排查:
棋盘坐标 (Z-depth) :AR 相机的剪裁平面(Clipping Planes)通常设置得比较窄。如果你的棋盘 Z 轴坐标设置成了 0,而相机在 -10 且正交尺寸不对,棋盘可能就在你的视线之外。
Layer 问题 :检查生成的 Board 节点是否在默认层。如果误分到了不可见的 Layer,它就像隐身了一样。
Inspector 赋值检测 :如果是通过编辑器拖入的预制体,检查 gemPrefabs 数组有没有漏填。哪怕空了一项,代码在实例化时就会中断,导致整个棋盘崩溃。
5.3 构建设置与分辨率
色彩空间 :尽量选择 Linear。
渲染管线 :建议开启 URP,以获得更好的抗锯齿效果,否则宝石边缘会有严重的锯齿感。
多线程渲染 :开启它可以显著降低眼镜的功耗和发热。
实机体验与价值 当这个'轻量级'消消乐真正跑在 Rokid AR Lite 上时,其带来的产品启示远超游戏本身。
6.1 实机体感 在办公室实测时,我最喜欢的一种体验是:将棋盘挂在显示器的正上方。当我写代码写累了,不需要低头找手机(这个动作对颈椎极不友好),只需要轻轻一抬头,对着空中的宝石滑几下。
总结与展望 利用 Unity 2022 LTS + UXR 3.0 SDK ,我们完成了一个从零到一的 AR 探索。这套组合的优点显而易见:生态成熟、开发门槛低、真机适配顺滑。
当然,目前这仅仅是一个'毛坯房'。接下来,我们可以迭代的方向还有很多:
音效与反馈 :加入立体空间音频,让宝石爆破的声音从棋盘所在的方位传来。
视觉特效 :引入 URP 的粒子系统,让消除瞬间火花四溅。
深度交互 :接入手势追踪。想象一下,用食指和中指'捏'住宝石进行物理拖拽,那才是真正的空间计算体验。
关卡设计 :引入不同的障碍物(冰块、木板),将这套轻量级原型打造成完整的商业产品。
AR 开发不需要一上来就追求宏大的叙事,有时候,在现实世界里安安稳稳地玩上一局消消乐,就是空间计算带给我们的最纯粹的快乐。
微信扫一扫,关注极客日志 微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具 加密/解密文本 使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown 转 HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML 转 Markdown 互为补充。 在线工具,Markdown 转 HTML在线工具,online
HTML 转 Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML 转 Markdown在线工具,online
JSON 压缩 通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online