游戏账号大图生成

1 背景

转转游戏账号业务中,以王者荣耀为例,用户关注的核心指标是:英雄总数、皮肤总数、特色皮肤等重点信息。但是在旧版商品列表展示的图片,是用户上传的图片,以及一些简单业务规则生成的图片,没有突出账号相关重点特色。

虽然在商详(商品详情页)内部已经呈现出了完整的验号报告,但是用户需要从商列(商品列表页)点击进入到商详,存在较长的转换路径,降低了关键信息的触达效率。

于是我们将游戏账号相关重点信息生成大图,展示在商列及商详。

商列(商品列表页)大图:

商详(商品详情页)大图:

2 从前端生成大图,到后端生成大图

2.1 前端生成大图的考量与实现方案

需求初期,大图由前端生成,主要出于以下考虑:

  1. 前端可利用浏览器原生渲染能力处理复杂布局和样式,开发验证速度较快。
  2. 前端能更灵活地支持图像处理,开发成本较低。

前后端进行交互,由后端提供生成图片所需的全量物料图片信息。前端拿到物料之后,通过页面api,与Puppeteer截图相结合的方式生成大图。步骤如下:

  1. 前端创建一个页面,通过访问后端接口拿到对应的物料数据(小图url、文字等)。对这些图片资源url数据进行实时访问,在页面上进行图片与文字的绘制,进而生成大图的页面。
  2. 将此页面的地址,传到另一个Puppeteer服务。Puppeteer服务会启动一个浏览器实例,再次访问传入的页面,然后进行截图。

2.2 前端生成大图遇到的问题及原因分析

在上线之后,发现前端生成大图会有一定概率的超时异常导致生成图片失败,平均2~3s,超时情况大于5s。在生成只有12个拼接图片的情况下尚且超时,随着我们商详大图需求的引入,拼接图片数量有时会超过600。此时超时情况会更加严重。

通过对整体链路的分析,发现Puppeteer服务截图是一个耗时较多的操作,其大概步骤如下:

  • 申请服务内存、磁盘等资源;
  • 创建浏览器进程并启动;
  • 通过网络访问传入页面URL;
  • 渲染页面并且进行截图。

2.3 改进方案-后端生成大图

为了解决前端方案的性能瓶颈,我们评估了迁移到后端生成图片的可行性。对于上面遇到的问题,后端可以有针对性地进行解决:使用Java中awt包下的画图api拼接生成大图,避免了Puppeteer启动浏览器、渲染页面带来的开销。

能否迁移到后端,有两个衡量标准,第一个是性能,即耗时。第二个是后端生成的图片UI样式,即能否达到UI验收标准。

于是我们先在本地测试,发现相同图片耗时仅仅需要20ms左右(相较前端平均2~3秒的时间有较大提升)。即使涉及500多张图片的拼接,平均耗时也只在2s左右。另外在生成的图片样式效果上也达到了UI验收的要求。

前端生成大图:

后端生成大图:

3. 后端生成大图的具体实现

以上文提到的、规则相对复杂的商品详情页大图为例进行说明。通过分析UI原型,我们发现其结构具有清晰的模块化特征:从上往下看依次为基础信息模块、分类皮肤信息模块。其中皮肤信息模块又可分为分类标题模块、单个皮肤单元模块。

这里需要用到的操作,包括绘制图片、文字、伸缩图片、平移图片等。由于后端使用的编程语言为Java,所以先简要介绍一下Java图片处理相关的api。

3.1 Java图像处理api

在Java图像处理中,java.awt.image.BufferedImage.BufferedImage与java.awt.Graphics2D是两个核心类,它们密切协作以实现图像的创建、编辑和渲染。

  • BufferedImage是图像数据的画布容器,负责存储像素信息。主要用于读写图像文件。
  • Graphics2D是操作图像的画笔,负责绘制和修改图像内容。在BufferedImage上绘制内容。支持绘制文字、图像平移缩放等操作。

3.1.1 创建BufferedImage

ini 复制代码
    /**
     * 读取本地文件
     */
    BufferedImage imageFromFile = ImageIO.read(new File("本地图片路径"));
    /**
     * 从网络中读取图片
     */
    BufferedImage imageFromUrl= ImageIO.read(new URL("网络图片路径"));

    /**
     * 通过构造方法创建。构造参数指定宽和高。
    */
    BufferedImage combinedImage = new BufferedImage(100, 200, BufferedImage.TYPE_INT_RGB);

3.1.2 绘制图片与文字

java 复制代码
    public static void main(String[] args) throws IOException {
        BufferedImage backGroundImage = ImageIO.read(new File("输入路径"));

        BufferedImage combinedImage = new BufferedImage(backGroundImage.getWidth(), backGroundImage.getHeight(), BufferedImage.TYPE_INT_RGB);

        Graphics2D graphics = combinedImage.createGraphics();
        try {
            // 画图
            graphics.drawImage(backGroundImage, 0, 0, null);
            // 写文字
            graphics.setFont(new Font("微软雅黑", Font.BOLD, 20));
            graphics.setColor(Color.WHITE);
            graphics.drawString("文字内容", 20, 20);

        } finally {
            // 释放资源
            graphics.dispose();
        }

        // 保存结果
        File output = new File("输出路径");
        ImageIO.write(combinedImage, "jpg", output);
    }

3.1.3 图片伸缩

java 复制代码
    public static void main(String[] args) throws IOException {
       // 伸缩前的画面
        BufferedImage originImage = ImageIO.read(new File("文件路径"));

        // 伸缩后的画面
        BufferedImage scaleImage = getScaleImage(originImage, 0.7, 0.7);
        File output = new File("输出路径");
        ImageIO.write(scaleImage, "jpg", output);
 }

    /**
     * 对原始图片按比例进行伸缩
     */
    public static BufferedImage getScaleImage(BufferedImage originImage, double scaleFactorWidth, double scaleFactorHeight) {
        if (Objects.isNull(originImage) || scaleFactorWidth <= 0 || scaleFactorHeight <= 0) {
            return originImage;
        }
        // 等比例压缩比例
        int scaledWidth = (int) (originImage.getWidth() * scaleFactorWidth);
        int scaledHeight = (int) (originImage.getHeight() * scaleFactorHeight);

        // 创建新的 BufferedImage
        BufferedImage scaledImage = new BufferedImage(scaledWidth, scaledHeight, BufferedImage.TYPE_INT_RGB);

        Graphics2D g2d = scaledImage.createGraphics();
        try {
            // 绘制缩放后的图像
            g2d.drawImage(originImage, 0, 0, scaledWidth, scaledHeight, null);
        } finally {
            g2d.dispose();
        }
        return scaledImage;

3.2 基础信息部分

原始UI图如下,我们需要将具体文字信息写入到对应位置内。

实现思路:

  1. 首先将原始UI图绘制到画布;
  2. 设置文字字体、大小,以及横纵坐标参数,绘制文字。

效果图如下:

3.3 模块化拼接皮肤分类部分

皮肤分类模块分开来看,可以按照皮肤类型分成若干个大类,如史诗皮肤、限定皮肤等。同时在每个皮肤分类模块前有对应标题。

3.3.1 绘制标题

通过api绘制标题图片到指定位置即可,这样可省去字体设置与字体居中的步骤。

3.3.2 生成皮肤图片单元

每一个皮肤分类中,各个皮肤单元都由四部分元素组成,分别是角标图片、底图图片、文字、浮层图片。

我们拿到这四部分基础原始数据之后,按如下步骤进行绘制:

  1. 绘制皮肤底图到画布;
  2. 绘制浮层覆盖到皮肤底图之上;
  3. 绘制角标图片到指定位置;
  4. 绘制文字到指定位置。

由于各类小图片信息是短时间内不会变更的,所以这里会对各类小图片进行本地缓存,避免频繁网络请求导致的资源浪费。

3.3.3 生成皮肤分类模块

将所有生成的皮肤图片单元按指定横纵坐标绘制。

3.3.4 拉伸背景与边框

原始UI切图如下:

由于每个账号对应的皮肤数量不同,需要让背景图与边框适配对应数量的皮肤图片总高度。

3.3.5 拼接生成各分类组合大图

通过对每个皮肤分类,重复以上步骤,即可生成各分类组合大图。

4 总结

4.1 性能提升

生成耗时从平均2-3秒(前端+Puppeteer)降至毫秒级(简单图片)至秒级(超复杂图片如500+皮肤),解决了超时问题。

4.2 用户体验

确保了用户在浏览商品时,能快速地获取到游戏账号的核心价值信息。

4.3 扩展性与维护性

通过模块化思想,将图片拼接的核心逻辑(图片加载、绘制、文字渲染、布局、背景处理)抽象为可复用的基础服务模块。再结合动态配置来定义不同游戏的大图布局、元素样式、数据映射规则等,实现了业务逻辑与渲染逻辑的解耦。使新游戏品类的接入效率大幅提升。

现已应用在王者荣耀、原神、火影忍者、枪战王者等多款游戏。

原神: 火影忍者: 枪战王者:


关于作者

张廉洁 转转Java开发工程师

> 转转研发中心及业界小伙伴们的技术学习交流平台,定期分享一线的实战经验及业界前沿的技术话题。

> 关注公众号「转转技术」(综合性)、「大转转FE」(专注于FE)、「转转QA」(专注于QA),更多干货实践,欢迎交流分享~

相关推荐
双力臂4041 小时前
Java IO流体系详解:字节流、字符流与NIO/BIO对比及文件拷贝实践
java·开发语言·nio
钮钴禄·爱因斯晨2 小时前
Java API (二):从 Object 类到正则表达式的核心详解
java·开发语言·信息可视化·正则表达式
Monkey-旭2 小时前
Android 蓝牙通讯全解析:从基础到实战
android·java·microsoft·蓝牙通讯
BoneToBone3 小时前
java list 与set 集合的迭代器在进行元素操作时出现数据混乱问题及原因
java·开发语言·list
WanderInk4 小时前
深入解析:Java Arrays.sort(intervals, Comparator.comparingInt(a -> a[0])); 一行代码的背后功力
java·后端·算法
O执O4 小时前
JavaWeb笔记四
java·hive·hadoop·笔记·web
codeGoogle4 小时前
“ASIC项目90%会失败”,黄仁勋的“诅咒”劝退华为?
后端
追逐时光者4 小时前
一款基于 .NET 开源免费、轻量快速、跨平台的 PDF 阅读器
后端·.net
二十雨辰4 小时前
[尚庭公寓]11-用户与系统管理
java·服务器·spring
Dajiaonew4 小时前
从零搭建Cloud Alibaba
java·数据库·分布式·微服务