iogame服务端游戏框架整合springboot + mybatisplus

概述

​ 本次测试为了记录第一次尝试游戏开发的过程,项目使用了游戏服务端开源框架iogame iogame gitee源码,前端采用uniapp框架,Vue3+js,后端使用springboot+mybatisplus,数据库版本为mysql5.7结合的方式开发一个h5 demo。对于iogame框架不熟悉的同学可以先移步 iogame说明介绍文档 观看相关介绍文档。

使用框架组件版本备注
iogame17.1.38
JDK17
springboot2.7.0
mybatis-plus3.5.3.1
mysql5.7.40

一、加入相关依赖

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.3.1</version>
</dependency>

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.49</version>
</dependency>

二、修改application.properties配置

数据库使用了8.x以上的版本驱动为com.mysql.cj.jdbc.Driver

server.port=8001

spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/my_demo?useSSL=false&serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=123456

三、快速开始

先来看一下整体项目目录结构:

springboot项目目录结构

下面开始上代码。

1、创建测试表

CREATE TABLE `game_users` (
 `id` varchar(64) NOT NULL,
 `user_name` varchar(50) NOT NULL,
 `password` varchar(50) NOT NULL,
 `email` varchar(50) NOT NULL,
 `score` int(11) DEFAULT '0',
 PRIMARY KEY (`id`)
)

2、cmd

public interface LoginModule {
    int cmd = SpringCmdModule.loginCmd;

    int register = 0;

    int login = 1;
}

3、Action

@Slf4j
@Component
@ActionController(LoginModule.cmd)
public class UserAction {

    @Resource
    UserService userService;

    @ActionMethod(LoginModule.register)
    public String createUser(User user) {
        return userService.createUser(user);
    }
}

4、实体类

@Data
@ToString
@ProtobufClass
@FieldDefaults(level = AccessLevel.PUBLIC)
public class User {
    String id;
    String userName;
    String password;
    String email;
}

5、Mapper

@Mapper
public interface UserMapper extends BaseMapper<User> {

    void createUser(User user);
}

xml:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.io.game.login.mapper.UserMapper">

    <insert id="createUser">
        insert into game_users (id, user_name, password, email)
        values (#{id}, #{userName}, #{password}, #{email});
    </insert>
</mapper>

6、Service

public interface UserService extends IService<User> {

    String createUser(User user);
}

service实现类:

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {

    @Resource
    UserMapper userMapper;

    @Override
    public String createUser(User user) {
        user.setId(UUID.randomUUID().toString());
        userMapper.createUser(user);
        return user.getId();
    }
}

7、逻辑服

@Getter
@Setter
@FieldDefaults(level = AccessLevel.PRIVATE)
public class GameLoginClient extends AbstractBrokerClientStartup {
    @Override
    public BarSkeleton createBarSkeleton() {
        // 业务框架构建器 配置
        BarSkeletonBuilderParamConfig config = MyBarSkeletonConfig.createBarSkeletonBuilderParamConfig()
                // 扫描 action 类所在包
                .scanActionPackage(UserAction.class);

        // 业务框架构建器
        BarSkeletonBuilder builder = MyBarSkeletonConfig.createBarSkeletonBuilder(config);
        // 开启 jsr380 验证
        builder.getSetting().setValidator(true);

        return builder.build();
    }

    @Override
    public BrokerClientBuilder createBrokerClientBuilder() {
        BrokerClientBuilder builder = BrokerClient.newBuilder();
        builder.appName("spring login 登录游戏逻辑服");
        return builder;
    }
}

8、启动类

启动类设置了json编解码。

@MapperScan("com.io.game.login.mapper")
@ComponentScan("com.io.game.login")
@SpringBootApplication
public class LoginApplication {

    public static void main(String[] args) {
        // 设置 json 编解码。如果不设置,默认为 jprotobuf
        IoGameGlobalSetting.me().setDataCodec(new JsonDataCodec());
        // 启动 spring boot
        SpringApplication.run(LoginApplication.class, args);

        // 启动班级逻辑服
        GameLoginClient gameLogicClient = new GameLoginClient();
        BrokerClientApplication.start(gameLogicClient);
    }

    @Bean
    public ActionFactoryBeanForSpring actionFactoryBean() {
        // 将业务框架交给 spring 管理
        return ActionFactoryBeanForSpring.me();
    }
}

注意:

​ 因为我的启动类不和项目在一个模块,需要加上@ComponentScan(“com.io.game.login”)注解,不然会报 No qualifying bean of type ‘com.io.game.login.action.UserAction’ available 错误提示。

ation类报错信息

13:31:34.847 ERROR [uestMessage-8-2] .game.common.kit.log.IoGameLoggerFactory.handler(ActionCommandTryHandler.java:44) : No qualifying bean of type 'com.io.game.login.action.UserAction' available

org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.io.game.login.action.UserAction' available
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:351)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:342)
	at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1172)
	at com.iohao.game.action.skeleton.ext.spring.ActionFactoryBeanForSpring.getBean(ActionFactoryBeanForSpring.java:53)
	at com.iohao.game.action.skeleton.core.DependencyInjectionPart.getBean(DependencyInjectionPart.java:65)
	at com.iohao.game.action.skeleton.core.DefaultActionFactoryBean.getBean(DefaultActionFactoryBean.java:31)
	at com.iohao.game.action.skeleton.core.DefaultActionCommandFlowExecute.execute(DefaultActionCommandFlowExecute.java:53)
	at com.iohao.game.action.skeleton.core.ActionCommandHandler.handler(ActionCommandHandler.java:37)
	at com.iohao.game.action.skeleton.core.ActionCommandTryHandler.handler(ActionCommandTryHandler.java:42)
	at com.iohao.game.action.skeleton.core.BarSkeleton.handle(BarSkeleton.java:108)
	at com.iohao.game.bolt.broker.core.common.processor.hook.DefaultRequestMessageClientProcessorHook.lambda$processLogic$0(DefaultRequestMessageClientProcessorHook.java:74)
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
	at java.base/java.lang.Thread.run(Thread.java:833)

以及@MapperScan(“com.io.game.login.mapper”)注解,不加会提示找不到mapper。

mapper not found

13:40:44.746 ERROR [    main] agnostics.LoggingFailureAnalysisReporter.report(LoggingFailureAnalysisReporter.java:40) : 

***************************

APPLICATION FAILED TO START

***************************

Description:

A component required a bean of type 'com.io.game.login.mapper.UserMapper' that could not be found.


Action:

Consider defining a bean of type 'com.io.game.login.mapper.UserMapper' in your configuration.


Process finished with exit code 1

四、测试

​ 前端我使用的是uniapp框架(打算做一个h5小游戏),使用Vue3+js编写的一个简陋页面(由于本人主要做后端的,前端不是很熟悉,可能会有错误的地方,欢迎大家指正)

先看下前端整体项目目录结构:

前端项目目录结构

用户注册页面:
<template>
	<view>
		<input type="text" v-model="username" placeholder="Username" />
		<input type="password" v-model="password" placeholder="Password" />
		<input type="email" v-model="email" placeholder="Email" />
		<button @click="register">Register</button>
	</view>
	<view id="output" style="margin-top:20px;border: 1px #ddd solid; height: 260px; overflow-y: auto;" />
</template>


<script>
	import webSocket from '@/server/websocket.js';

const websocket = webSocket.addsocket("127.0.0.1:10100/websocket")

export default {
	data() {
		return {
			username: "",
			password: "",
			email: "",
		};
	},
	methods: {
		register() {
			const data = {
				"username": this.username,
				"password": this.password,
				"email": this.email
			}
			var message = {
			    cmdCode: 1,
			    cmd: 6,
				subCmd: 0,
			    data: data
			}
			webSocket.SendMessage(message)
		},
	},
};

</script>
websocket.js

发送消息最主要的方法是createExternalMessage,因为前后端通信使用的json编解码。

import $ from 'jquery'

function formatDate(now) {
    var year = now.getFullYear();
    var month = now.getMonth() + 1;
    var date = now.getDate();
    var hour = now.getHours();
    var minute = now.getMinutes();
    var second = now.getSeconds();
    return year + "-" + (month = month < 10 ? ("0" + month) : month) + "-" + (date = date < 10 ? ("0" + date) : date) + " " + (hour = hour < 10 ? ("0" + hour) : hour) + ":" + (minute = minute < 10 ? ("0" + minute) : minute) + ":" + (second = second < 10 ? ("0" + second) : second);
}

var websocket;

function addsocket(addr) {

    var wsaddr = "ws://" + addr;
    StartWebSocket(wsaddr);

}


function StartWebSocket(wsUri) {
    websocket = new WebSocket(wsUri);
    websocket.binaryType = 'arraybuffer';

    websocket.onopen = function (evt) {
        onOpen(evt, wsUri)
    };
    websocket.onclose = function (evt) {
        onClose(evt)
    };
    websocket.onmessage = function (evt) {
        onMessage(evt)
    };
    websocket.onerror = function (evt) {
        onError(evt)
    };

}

function onOpen(evt, wsUri) {
    writeToScreen("<span style='color:red'>连接成功,现在你可以发送信息进行测试了!</span>");
    writeToScreen(wsUri);
}

function onClose(evt) {
    writeToScreen("<span style='color:red'>Websocket连接已断开!</span>");
    websocket.close();
}

function binaryData(data) {
	if (data != null) {
		return JSON.parse(new TextDecoder("utf-8").decode(new Uint8Array(data)))
	}
}

function onMessage(evt) {
    let externalMessage = binaryData(evt.data);
    let bizData = externalMessage.data;
    let bizDataJson = binaryData(bizData);
    externalMessage.data = bizDataJson;

    let json = JSON.stringify(externalMessage);
    
    writeToScreen('<span style="color:blue">服务端回应&nbsp;' + formatDate(new Date()) + '</span><br/><span>' + json + '</span>');

}

function onError(evt) {
    writeToScreen('<span style="color: red;">发生错误:</span> ' + evt.data);
}

function SendMessage(data) {

    var externalMessageBytes = createExternalMessage(data);
    
    websocket.send(externalMessageBytes);

}

function createExternalMessage(data) {
	if (data != null) {
		console.log(data.data)
		var message = {
		    cmdCode: data.cmdCode,
		    cmdMerge: mergeCmd(data.cmd, data.subCmd),
		    data: data.data
		}
		

		var json = JSON.stringify(message);
		writeToScreen('<span style="color:green">你发送的信息&nbsp;' + formatDate(new Date()) + '</span><br/>' + json);
		
		var textEncoder = new TextEncoder();
		var dataArray = textEncoder.encode(JSON.stringify(data.data));
		
		message.data = Array.from(dataArray);
		
		json = JSON.stringify(message);
		
		return textEncoder.encode(json);
	}

}

function writeToScreen(message) {
    var div = "<div>" + message + "</div>";
    var d = $("#output");
    var d = d[0];
    var doScroll = d.scrollTop == d.scrollHeight - d.clientHeight;
    $("#output").append(div);
    if (doScroll) {
        d.scrollTop = d.scrollHeight - d.clientHeight;
    }
}

function mergeCmd(cmd,subCmd) {
	return (cmd << 16) + subCmd;
}

export default {
	addsocket,
	SendMessage,
	websocket
};
页面路由pages.json:
{
	"pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
		{
			"path": "pages/index/index",
			"style": {
				"navigationBarTitleText": "uni-app"
			}
		},
		{
            "path" : "pages/user/login",
            "style" :                                                                                    
            {
                "navigationBarTitleText": "",
                "enablePullDownRefresh": false
            }
            

        },
    	{
            "path" : "pages/user/register",
            "style" :                                                                                    
            {
                "navigationBarTitleText": "",
                "enablePullDownRefresh": false
            }
            
        }
    ],
    "globalStyle": {
    	"navigationBarTextStyle": "black",
    	"navigationBarTitleText": "uni-app",
    	"navigationBarBackgroundColor": "#F8F8F8",
    	"backgroundColor": "#F8F8F8"
    },
    "uniIdRouter": {}

}
服务端项目启动:

注意:

​ 服务端需要启动对外服通过websocket与前端建立连接,本次项目为了方便,使用了iogame示例spring项目,一键启动的方式(包括对外服,网关,逻辑服),并另外启动我们的登录逻辑服接入。

项目启动

前端项目启动:

项目启动

在页面输入账号密码进行测试:

打开http://localhost:5173/#/user/register,或者在前端HbuildX内置浏览器测试。

测试页面

查看服务端框架输出日志:

输出日志

查看数据库是否插入数据:

数据库表

数据成功插入,至此就圆满结束了!

踩坑记录:

一开始配置文件使用applicaton.yml

spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/my_demo?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
    username: root
    password: 123456
application:
  name: demo

mybatis-plus:
  # 数据库映射实体类包路径
  type-aliases-package: com.io.game.login.mapper
  mapper-locations: classpath:/mapper/*.xml
  configuration:
    # 是否开启自动驼峰命名规则(camel case)映射,即从经典数据库列名 A_COLUMN(下划线命名) 到经典 Java 属性名 aColumn(驼峰命名) 的类似映射
    map-underscore-to-camel-case: true

但是不知道为啥springboot读取不到配置文件,启动项目会报错。

***************************

APPLICATION FAILED TO START

***************************

Description:

Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured.

Reason: Failed to determine a suitable driver class


Action:

Consider the following:
	If you want an embedded database (H2, HSQL or Derby), please put it on the classpath.
	If you have database settings to be loaded from a particular profile you may need to activate it (no profiles are currently active).


Process finished with exit code 1

在网上百度了好久,报错的原因是使用了mybatis相关的数据库依赖,但是没有读取到配置文件。解决方法是@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)注解加上exclude = DataSourceAutoConfiguration.class才可以正常启动。

后面尝试使用application.properties配置文件,可以不需要加exclude = DataSourceAutoConfiguration.class,能正常读取到配置并启动。

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐