你知道MyBaits-Plus有哪些plus高级功能”玩法“吗?

简介: MyBaits-Plus提供的一些高级扩展功能,在日常开发中比较实用,都有相应的场景去使用,提高代码高效性,防止重复编码。

今天接着之前总结的入门教程分析:MyBatis-Plus最详细的入门教程,首先还是同样地需要准备一张表tb_user:

CREATE TABLE `tb_user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `user_no` varchar(255) NOT NULL COMMENT '编号',
  `nickname` varchar(255) DEFAULT NULL COMMENT '昵称',
  `email` varchar(255) DEFAULT NULL COMMENT '邮箱',
  `phone` varchar(255) NOT NULL COMMENT '手机号',
  `gender` tinyint(4) NOT NULL DEFAULT '0' COMMENT '性别  0:男生   1:女生',
  `birthday` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '出生日期',
  `is_delete` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标志 0:否  1:是',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  `create_by` bigint(20) DEFAULT NULL COMMENT '创建人',
  `update_by` bigint(20) DEFAULT NULL COMMENT '更新人',
  `address` varchar(1024) DEFAULT NULL COMMENT '地址',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;

在项目服务中对应的实体类User:

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@TableName(value = "tb_user")
public class User {
   
    @TableId(type = IdType.AUTO)
    private Long id;
    private String userNo;
    private String nickname;
    private String email;
    private String phone;
    private Integer gender;
    private Date birthday;
    private Integer isDelete;
    private Date createTime;
    private Date updateTime;
}

下面高级功能的示例都是基于上面的表进行展开的。

1.批量插入

我们在日常开发中知道大批量插入数据可能造成性能瓶颈,所以需要格外关注。在之前的入门教程讲过mp(MyBatis-Plus简称,下文都用简称)对数据库的CRUD操作提供了service层和mapper层的接口方法封装,两者的一大区别就是service CRUD接口提供了批量保存的操作,下面就分别来看看批量保存1000条user数据,然后对执行时间进行统计对比和性能评估,这里我连接的数据库是4核8G10M轻量服务器部署的,不同的数据库服务环境配置,执行效率是不一样的,所以下面的执行时间仅供对比参考。

mapper层的CRUD接口

    /**
     * mapper层的crud接口方法批量插入
     */
    @Test
    public void testMapperBatchAdd() {
   
        List<User> users = new ArrayList<>();
        for(long i = 1; i <= 1000; i++) {
   
            User user = User.builder()
                    .id(i)
                    .userNo("No-" + i)
                    .nickname("哈哈")
                    .phone("12345678901")
                    .email("shepherd_123@qq.com")
                    .birthday(new Date())
                    .gender(0)
                    .isDelete(0)
                    .build();
            users.add(user);
        }
        long start = System.currentTimeMillis();
        users.forEach(user -> {
   
            userDAO.insert(user);
        });
        long end = System.currentTimeMillis();
        System.out.println("执行时长:" + (end-start) + "毫秒");
    }

控制台输出如下:

执行时长:42516毫秒

service层CRUD接口

    /**
     * service层的crud接口方法批量插入
     */
    @Test
    public void testServiceBatchAdd() {
   
        List<User> users = new ArrayList<>();
        for(long i = 1; i <= 1000; i++) {
   
            User user = User.builder()
                    .id(i)
                    .userNo("No-" + i)
                    .nickname("哈哈")
                    .phone("12345678901")
                    .email("shepherd_123@qq.com")
                    .birthday(new Date())
                    .gender(0)
                    .isDelete(0)
                    .build();
            users.add(user);
        }
        long start = System.currentTimeMillis();
        userService.saveBatch(users);
        long end = System.currentTimeMillis();
        System.out.println("执行时长:" + (end-start) + "毫秒");
    }

执行结果:

执行时长:19385毫秒

可以看出,使用service层提供的批量保存接口userService.saveBatch(users)虽然快了很多,却仍需要19s,耗时比起mapper层的一条一条地插入快了23s,但是对于服务接口响应来说还是不可接受的。那为什么批量插入保存还是比较慢呢?

MySQL 的 JDBC 连接的 url 中要加 rewriteBatchedStatements 参数,并保证 5.1.13 以上版本的驱动,才能实现高性能的批量插入。

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: root
    url: jdbc:mysql://ip:3306/db_test?&serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=UTF8&autoReconnect=true&useSSL=false&allowPublicKeyRetrieval=true&rewriteBatchedStatements=true

再次执行上面的service层crud接口批量插入方法,执行结果如下:

执行时长:1364毫秒

可以看到 jdbcurl 添加了 rewriteBatchedStatements=true 参数后,批量操作的执行耗时已经只有 1364 毫秒快的飞起,具体缘由分析请看:https://juejin.cn/post/7295688187752562751

2.逻辑删除

现今互联网系统数据安全越发重要,逻辑删表是指在删除表中数据时,并不是直接将数据从表中删除,而是将数据的状态标记为已删除。这种方式被称为逻辑删除,与之相对的是物理删除。逻辑删除可以保留数据的完整性,同时也方便数据恢复。

添加配置如下:

mybatis-plus:
  global-config:
    db-config:
      logic-delete-field: isDelete # 全局逻辑删除的实体字段名
      logic-delete-value: 1 # 逻辑已删除值(默认为 1)
      logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)

测试样例:

    /**
     * 逻辑删除
     */
    @Test
    public void testLogicDelete() {
   
        userDAO.deleteById(1L);
    }

控制台输出如下:

2023-11-16 17:01:47.656 DEBUG 10649 --- [           main] c.s.m.demo.dao.UserDAO.deleteById        : ==>  Preparing: UPDATE tb_user SET is_delete=1 WHERE id=? AND is_delete=0
2023-11-16 17:01:47.702 DEBUG 10649 --- [           main] c.s.m.demo.dao.UserDAO.deleteById        : ==> Parameters: 1(Long)
2023-11-16 17:01:47.744 DEBUG 10649 --- [           main] c.s.m.demo.dao.UserDAO.deleteById        : <==    Updates: 1

从日志可以看出做了更新,这就是逻辑删除,接下来我们看看查询:

   @Test
    public void testQuery() {
        User user = userDAO.selectById(1L);
    }
2023-11-16 17:06:19.802 DEBUG 10687 --- [           main] c.s.m.demo.dao.UserDAO.selectById        : ==>  Preparing: SELECT id,user_no,nickname,email,phone,gender,birthday,is_delete,create_time,update_time FROM tb_user WHERE id=? AND is_delete=0
2023-11-16 17:06:19.851 DEBUG 10687 --- [           main] c.s.m.demo.dao.UserDAO.selectById        : ==> Parameters: 1(Long)
2023-11-16 17:06:19.896 DEBUG 10687 --- [           main] c.s.m.demo.dao.UserDAO.selectById        : <==      Total: 0

可以知道加上了未删除标志位这个条件is_delete=0,注意逻辑删除会造成唯一索引冲突

建立唯一索引时,需要额外增加 delete_time 字段,添加到唯一索引字段中,避免唯一索引冲突。例如说,tb_user 使用 user_no 作为唯一索引:

  • 未添加前:先逻辑删除了一条 user_no = 001 的记录,然后又插入了一条 user_no = 001 的记录时,会报索引冲突的异常。
  • 已添加后:先逻辑删除了一条 user_no = 001 的记录并更新 delete_time 为当前时间,然后又插入一条 user_no = 001 并且 delete_time 为 0 的记录,不会导致唯一索引冲突。

3.默认字段填充

按照一般设计规范,数据库表都有这几个字段创建时间(create_time)、更新时间(update_time)、创建人(create_by)、更新人(update_by),所以我抽取一个基础的DO类:BaseDO

/**
 * 数据库表字段公共属性抽象类
 */
@Data
public class BaseDO implements Serializable {
    /**
     * 创建时间
     */
    @TableField(fill = FieldFill.INSERT)
    private Date createTime;
    /**
     * 最后更新时间
     */
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Date updateTime;
    /**
     * 创建者
     */
    @TableField(fill = FieldFill.INSERT)
    private Long createBy;
    /**
     * 更新者
     */
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long updateBy;
    /**
     * 是否删除
     */
//    @TableLogic
//    private Integer deleted;
}

定义一个类实现MetaObjectHandler接口完成自动填充逻辑:DefaultDBFieldHandler

/**
 * 公共字段属性值自动填充
 */
public class DefaultDBFieldHandler implements MetaObjectHandler {
   

    @Override
    public void insertFill(MetaObject metaObject) {
   
        if (Objects.nonNull(metaObject) && metaObject.getOriginalObject() instanceof BaseDO) {
   
            BaseDO baseDO = (BaseDO) metaObject.getOriginalObject();

            Date current = new Date();
            // 创建时间为空,则以当前时间为插入时间
            if (Objects.isNull(baseDO.getCreateTime())) {
   
                baseDO.setCreateTime(current);
            }
            // 更新时间为空,则以当前时间为更新时间
            if (Objects.isNull(baseDO.getUpdateTime())) {
   
                baseDO.setUpdateTime(current);
            }

            baseDO.setCreateBy(1001L);
            baseDO.setUpdateBy(1002L);

//            // 根据登录上下文信息设置创建人和更新人
//            LoginUser currentUser = RequestUserHolder.getCurrentUser();
//            // 当前登录用户不为空,创建人为空,则当前登录用户为创建人
//            if (Objects.nonNull(currentUser) && Objects.isNull(baseDO.getCreator())) {
   
//                baseDO.setCreator(currentUser.getId());
//            }
//            // 当前登录用户不为空,更新人为空,则当前登录用户为更新人
//            if (Objects.nonNull(currentUser) && Objects.isNull(baseDO.getUpdater())) {
   
//                baseDO.setUpdater(currentUser.getId());
//            }
        }
    }

    @Override
    public void updateFill(MetaObject metaObject) {
   
        // 更新时间为空,则以当前时间为更新时间
        Object modifyTime = getFieldValByName("updateTime", metaObject);
        if (Objects.isNull(modifyTime)) {
   
            setFieldValByName("updateTime", new Date(), metaObject);
        }
        Object modifier = getFieldValByName("updateBy", metaObject);
        if (Objects.isNull(modifier)) {
   
            setFieldValByName("updateBy", 1002L, metaObject);
        }

//        LoginUser currentUser = RequestUserHolder.getCurrentUser();
//        // 当前登录用户不为空,更新人为空,则当前登录用户为更新人
//        if (Objects.nonNull(currentUser) && Objects.isNull(modifier)) {
   
//            setFieldValByName("updater", currentUser.getId(), metaObject);
//        }

    }
}

注入容器:

@Bean
public MetaObjectHandler defaultMetaObjectHandler(){
   
    return new DefaultDBFieldHandler();
}

测试示例:让上面的User类继承BaseDO

  @Test
  public void testAutoFieldFill() {
   
      User user = User.builder()
              .id(1001L)
              .userNo("No-001")
              .nickname("哈哈")
              .phone("12345678901")
              .email("shepherd_123@qq.com")
              .birthday(new Date())
              .gender(0)
              .isDelete(0)
              .build();
      userDAO.insert(user);
  }

上面我们并没有对创建时间,创建人等字段设置值,执行控制台输出如下:

2023-11-16 18:16:41.225 DEBUG 11359 --- [           main] c.s.mybatisplus.demo.dao.UserDAO.insert  : ==>  Preparing: INSERT INTO tb_user ( id, user_no, nickname, email, phone, gender, birthday, is_delete, create_time, update_time, create_by, update_by ) VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? )
2023-11-16 18:16:41.267 DEBUG 11359 --- [           main] c.s.mybatisplus.demo.dao.UserDAO.insert  : ==> Parameters: 1001(Long), No-001(String), 哈哈(String), shepherd_123@qq.com(String), 12345678901(String), 0(Integer), 2023-11-16 18:16:40.968(Timestamp), 0(Integer), 2023-11-16 18:16:41.018(Timestamp), 2023-11-16 18:16:41.018(Timestamp), 1001(Long), 1002(Long)
2023-11-16 18:16:41.272 DEBUG 11359 --- [           main] c.s.mybatisplus.demo.dao.UserDAO.insert  : <==    Updates: 1

可以看到四个基础字段都进行了值填充。

项目推荐:基于SpringBoot2.x、SpringCloud和SpringCloudAlibaba企业级系统架构底层框架封装,解决业务开发时常见的非功能性需求,防止重复造轮子,方便业务快速开发和企业技术栈框架统一管理。引入组件化的思想实现高内聚低耦合并且高度可配置化,做到可插拔。严格控制包依赖和统一版本管理,做到最少化依赖。注重代码规范和注释,非常适合个人学习和企业使用

Github地址https://github.com/plasticene/plasticene-boot-starter-parent

Gitee地址https://gitee.com/plasticene3/plasticene-boot-starter-parent

微信公众号Shepherd进阶笔记

交流探讨qun:Shepherd_126

4.字段类型处理

我们平时会碰到有些表字段是一个"复杂"字段类型,比如说是个JSON或者数组,这时候需要手动处理之后保存,查询之后也需要自己处理转换,虽然不难但是繁杂,所以mp针对这一情况也做了对应功能,比如每个人都包含一个户籍地址,如果地址对象类是Address,那个user就包含一个address属性:

首先数据库tb_user表添加添加一个地址字段address,存的是JSON字符串,所以长度大一些varchar(1024),接着定义地址类Address

@Data
public class Address {
   
    private Long id;
    private String province;
    private String city;
    private String region;
    private String address;
}

实体类中添加地址address属性:

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@TableName(value = "tb_user", autoResultMap = true)
public class User extends BaseDO {
   

    @TableId(type = IdType.AUTO)
    private Long id;

    private String userNo;

    private String nickname;

    private String email;

    private String phone;

    private Integer gender;

    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date birthday;

    private Integer isDelete;

    @TableField(typeHandler = JacksonTypeHandler.class)
    private Address address;

}

测试示例:

  @Test
  public void testTypeHandler() {
   
      Address address = new Address();
      address.setProvince("浙江省");
      address.setCity("杭州市");
      address.setRegion("余杭区");
      address.setAddress("城北万象城");
      address.setId(1L);
      User user = User.builder()
              .id(100L)
              .userNo("No-001")
              .nickname("哈哈")
              .phone("12345678901")
              .email("shepherd_123@qq.com")
              .birthday(new Date())
              .gender(0)
              .isDelete(0)
              .address(address)
              .build();
      userDAO.insert(user);
  }
2023-11-16 18:37:58.251 DEBUG 11546 --- [           main] c.s.mybatisplus.demo.dao.UserDAO.insert  : ==>  Preparing: INSERT INTO tb_user ( id, user_no, nickname, email, phone, gender, birthday, is_delete, address, create_time, update_time, create_by, update_by ) VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? )
2023-11-16 18:37:58.333 DEBUG 11546 --- [           main] c.s.mybatisplus.demo.dao.UserDAO.insert  : ==> Parameters: 100(Long), No-001(String), 哈哈(String), shepherd_123@qq.com(String), 12345678901(String), 0(Integer), 2023-11-16 18:37:57.997(Timestamp), 0(Integer), {"id":1,"province":"浙江省","city":"杭州市","region":"余杭区","address":"城北万象城"}(String), 2023-11-16 18:37:58.039(Timestamp), 2023-11-16 18:37:58.039(Timestamp), 1001(Long), 1002(Long)
2023-11-16 18:37:58.336 DEBUG 11546 --- [           main] c.s.mybatisplus.demo.dao.UserDAO.insert  : <==    Updates: 1

查询示例:

    @Test
    public void testQuery() {
   
        User user = userDAO.selectById(100L);
        System.out.println(user);
    }
2023-11-16 18:42:47.023 DEBUG 11564 --- [           main] c.s.m.demo.dao.UserDAO.selectById        : ==>  Preparing: SELECT id,user_no,nickname,email,phone,gender,birthday,is_delete,address,create_time,update_time,create_by,update_by FROM tb_user WHERE id=?
2023-11-16 18:42:47.051 DEBUG 11564 --- [           main] c.s.m.demo.dao.UserDAO.selectById        : ==> Parameters: 100(Long)
2023-11-16 18:42:47.164 DEBUG 11564 --- [           main] c.s.m.demo.dao.UserDAO.selectById        : <==      Total: 1
User(id=100, userNo=No-001, nickname=哈哈, email=shepherd_123@qq.com, phone=12345678901, gender=0, birthday=Thu Nov 16 18:37:58 CST 2023, isDelete=0, address=Address(id=1, province=浙江省, city=杭州市, region=余杭区, address=城北万象城))

可以看出复杂类型对象字段都自动转换了,注意在实体类上一定要指定@TableName(autoResultMap = true)

之前我们分析过的字段加密存储,也是通过类型转换器实现,详见:Spring Boot如何优雅实现数据加密存储

5.动态表名

业务系统随着使用时间的推移数据量越来越大,所以我们会对数据量大的表进行归档处理,比如说通过日期进行了水平分表,需要通过日期参数,动态的查询数据,Mybatis-plus可以通过此插件解析替换设定表名为处理器的返回表名

插件配置

    @Bean
    public MybatisPlusInterceptor paginationInterceptor() 
    {
   
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        //动态表名
        interceptor.addInnerInterceptor(dynamicTableNameInnerInterceptor());
   }

    @Bean
    public DynamicTableNameInnerInterceptor dynamicTableNameInnerInterceptor() 
    {
   
        DynamicTableNameInnerInterceptor dynamicTableNameInnerInterceptor =new DynamicTableNameInnerInterceptor();
        //
        TableNameHandler tableNameHandler=new MyTableNameHandler();
        dynamicTableNameInnerInterceptor.setTableNameHandler(tableNameHandler);
        return dynamicTableNameInnerInterceptor;
    }

动态表名规则处理类

public class MyTableNameHandler implements TableNameHandler
{
   
    private Logger logger = LoggerFactory.getLogger(MyTableNameHandler.class);
    @Override
    public String dynamicTableName(String sql, String tableName)
    {
   
        logger.info("dynamicTableName sql:{},tableName:{}",sql,tableName);

        //如果参数为空,则以不要动态动态查询表名
        Map<String, Object> paramMap = RequestDataHelper.getRequestData();
        if(paramMap==null || paramMap.isEmpty())
        {
   
            logger.info("dynamicTableName paramMap is null");
            return tableName;
        }

        // 获取参数方法
        paramMap.forEach((k, v) -> logger.info(k + "----" + v));

        int random = (int) paramMap.get("tableNo");
        String tableNo = "_1";
        if (random % 2 == 1) 
        {
   
            tableNo = "_2";
        }
        String queryTableName=tableName + tableNo;
        logger.info("---------> queryTableName:{}",queryTableName);
        return queryTableName;
    }
}

参数上下文传递

public class RequestDataHelper
{
   
    /**
     * 请求参数存取
     */
    private static final ThreadLocal<Map<String, Object>> REQUEST_DATA = new ThreadLocal<>();

    /**
     * 设置请求参数
     *
     * @param requestData 请求参数 MAP 对象
     */
    public static void setRequestData(Map<String, Object> requestData) {
   
        REQUEST_DATA.set(requestData);
    }


    /**
     * 获取请求参数
     *
     * @return 请求参数 MAP 对象
     */
    public static Map<String, Object> getRequestData() 
    {
   
        return REQUEST_DATA.get();
    }
}

6.多租户插件

关于多租户的使用场景和功能实现,插件使用,之前我们已经总结分析过了,传送门:浅析SaaS多租户系统数据隔离实现方案

7.总结

以上全部就是mp提供的一些高级扩展功能,在日常开发中比较实用,都有相应的场景去使用,提高代码高效性,防止重复编码。

目录
相关文章
|
8月前
|
机器学习/深度学习 前端开发
前端移动web高级详细解析二(2)
02-动画 过渡:实现两个状态间的变化过程 动画:实现多个状态间的变化过程,动画过程可控(重复播放、最终画面、是否暂停) 动画实现步骤
79 0
|
3月前
|
Java 数据处理 Android开发
深入解析移动应用开发:从基础到高级
【10月更文挑战第2天】本文将带你深入探索移动应用开发的世界,从基础知识到高级技术,我们将一一解析。无论你是初学者还是有经验的开发者,这篇文章都将为你提供有价值的信息和知识。我们将从移动操作系统的选择开始,然后深入探讨移动应用开发的流程和技术,最后还将分享一些实用的代码示例。让我们一起开启这场移动应用开发的旅程吧!
|
4月前
|
安全 前端开发 关系型数据库
深入理解后端开发:从基础到高级实践
本文将带你走进后端开发的奇妙世界,从最基础的概念开始,逐步深入到高级实践。无论你是初学者还是有一定经验的开发者,都能在这篇文章中找到有价值的信息。我们将一起探讨后端开发的核心概念、常用技术栈、以及如何在实际项目中应用这些知识。准备好了吗?让我们一起开启这场后端开发的探索之旅吧!
144 4
|
1月前
|
存储 前端开发 Java
深入理解后端开发:从基础到高级
本文将带你走进后端开发的神秘世界,从基础概念到高级应用,一步步揭示后端开发的全貌。我们将通过代码示例,让你更好地理解和掌握后端开发的核心技能。无论你是初学者还是有一定经验的开发者,这篇文章都将为你提供有价值的信息和启示。
|
3月前
|
存储 网络协议 关系型数据库
探索后端开发:从基础到高级
【9月更文挑战第32天】在这篇文章中,我们将一起探索后端开发的奥秘。无论你是初学者还是有经验的开发者,这篇文章都将为你提供有价值的信息。我们将从基础知识开始,然后逐步深入到更复杂的主题。最后,我们将通过一些代码示例来展示这些概念的应用。让我们一起开始这段旅程吧!
|
4月前
|
Java Linux Android开发
深入理解Android开发:从基础到高级
【9月更文挑战第17天】本文将深入探讨Android开发的各个方面,包括应用开发、操作系统等。我们将通过代码示例来展示如何创建一个简单的Android应用,并解释其背后的原理。无论你是初学者还是有经验的开发者,这篇文章都将为你提供有价值的信息和启示。
|
4月前
|
存储 Java 数据库
探索后端开发:从基础到高级实践
【9月更文挑战第12天】本文深入探讨了后端开发的核心概念、关键技术和进阶实践。我们将通过一个实际的代码示例,展示如何搭建一个简单的后端服务,并解释其背后的原理。无论你是初学者还是有经验的开发者,这篇文章都将为你提供有价值的见解和技巧。
|
5月前
|
前端开发 Java PHP
探索后端开发之旅:从基础到高级
【8月更文挑战第28天】本文将带领读者走进后端开发的奇妙世界,从基础概念出发,逐步深入到高级技术。我们将通过易懂的语言和生动的比喻,让读者轻松理解后端开发的精髓。同时,文章还将分享一些实用的技巧和建议,帮助读者在后端开发的道路上越走越远。让我们一起踏上这场充满挑战与乐趣的旅程吧!
|
4月前
|
存储 JavaScript NoSQL
探索后端开发:从基础到高级的实践之路
在数字化时代,后端开发是构建强大、可靠和高效软件系统的基石。本文将引导你了解后端开发的各个方面,包括语言选择、框架应用、数据库管理、API设计、安全性考虑及性能优化等关键要素。无论你是初学者还是有一定经验的开发者,都能通过这些实用技巧和最佳实践,提升你的后端开发技能。
45 0
|
5月前
|
开发者 图形学 iOS开发
掌握Unity的跨平台部署与发布秘籍,让你的游戏作品在多个平台上大放异彩——从基础设置到高级优化,深入解析一站式游戏开发解决方案的每一个细节,带你领略高效发布流程的魅力所在
【8月更文挑战第31天】跨平台游戏开发是当今游戏产业的热点,尤其在移动设备普及的背景下更为重要。作为领先的游戏开发引擎,Unity以其卓越的跨平台支持能力脱颖而出,能够将游戏轻松部署至iOS、Android、PC、Mac、Web及游戏主机等多个平台。本文通过杂文形式探讨Unity在各平台的部署与发布策略,并提供具体实例,涵盖项目设置、性能优化、打包流程及发布前准备等关键环节,助力开发者充分利用Unity的强大功能,实现多平台游戏开发。
157 0

热门文章

最新文章