基于cocos + Java + netty 开发三国杀游戏(1)

本人严重拖延症,拖了好久才开始弄这个三国杀。

首先,先弄好cocos的相关界面与请求。

1.搭建webSocket连接脚本。

2.搭建登入,和注册界面。

首先,我们先搭建一个简单的webSocket连接

const { ccclass, property } = cc._decorator;
const serverAddress = "ws://localhost:8082";
@ccclass
// WebSocketClient.ts
export default class WebSocketClient {
    private static _instance: WebSocketClient;
    private socket: WebSocket | null = null;

    private constructor() {
        // 初始化WebSocket连接
        this.socket = new WebSocket(serverAddress);

        // 设置WebSocket事件监听
        this.socket.onopen = this.onWebSocketOpen.bind(this);
        this.socket.onmessage = this.onWebSocketMessage.bind(this);
        this.socket.onclose = this.onWebSocketClose.bind(this);
        this.socket.onerror = this.onWebSocketError.bind(this);
    }

    public static get instance(): WebSocketClient {
        if (!this._instance) {
            this._instance = new WebSocketClient();
        }
        return this._instance;
    }

    private onWebSocketOpen(event: Event) {
        console.log("WebSocket连接已建立");
    }

    private onWebSocketMessage(event: MessageEvent) {
        console.log("收到消息" + event.data)
    }

    private onWebSocketClose(event: CloseEvent) {
        console.log("WebSocket连接已关闭");
    }

    private onWebSocketError(event: Event) {
        console.error("WebSocket连接发生错误");
    }

    public sendPlayerMove(data: any) {
        // 确保WebSocket连接已建立后再发送消息
        if (this.socket && this.socket.readyState === WebSocket.OPEN) {
            console.log('发送数据:' + JSON.stringify(data))
            this.socket.send(JSON.stringify(data));
        } else {
            console.log("WebSocket连接尚未建立,无法发送消息。");
        }
    }

    public close() {
        if (this.socket) {
            this.socket.close();
        }
    }
}

然后,我们再搭建一个登入注册的脚本,并且把它挂载再Cocos的界面上

import WebSocketClient from "../socket";

const {ccclass, property} = cc._decorator;
// 这个接口的目的是,便于后端接收数据 定义的数据类型,不知道能不能叫做协议
export interface user{
    code:number
    userName:string
    passWord:string
    nickName?:string
}

@ccclass
export  class login extends  cc.Component {

    @property(cc.EditBox)
    userName: cc.EditBox = null;
    @property(cc.EditBox)
    passWord: cc.EditBox = null;
    // 登入按鈕
    @property(cc.Button)
    btnLogin: cc.Button = null;
    // 注册按钮
    @property(cc.Button)
    btnReg: cc.Button = null;

    @property(cc.Node)
    regBg: cc.Node = null;
    @property(cc.Button)
    yes: cc.Button = null;
    @property(cc.Button)
    no: cc.Button = null;
    @property(cc.EditBox)
    userName2: cc.EditBox = null;
    @property(cc.EditBox)
    passWord2: cc.EditBox = null;
    @property(cc.EditBox)
    nick: cc.EditBox = null;


    private socket : WebSocketClient;

    protected onLoad(): void {
        this.regBg.active = false;
        this.btnLogin.node.on(cc.Node.EventType.MOUSE_DOWN,this.goLogin,this);
        this.btnReg.node.on(cc.Node.EventType.MOUSE_DOWN,()=>{
            console.log("按下注册按钮")
            this.regBg.active = true;
        });

        this.yes.node.on(cc.Node.EventType.MOUSE_DOWN,()=>{
            const userInfo:user = {
                userName: this.userName2.textLabel.string,
                passWord: this.passWord2.textLabel.string,
                nickName: this.nick.textLabel.string,
                code: 1002
            }
            this.socket.sendPlayerMove(userInfo)
        });

        this.no.node.on(cc.Node.EventType.MOUSE_DOWN,()=>{
            this.regBg.active = false;
        },this);
        this.socket =  WebSocketClient.instance
    }
    //登入
    goLogin(){
        if(this.userName.textLabel.string.length<=0 || this.passWord.textLabel.string.length<=0){
            return;
        }

        const userInfo:user = {
            userName: this.userName.textLabel.string,
            passWord: this.passWord.textLabel.string,
            code: 1001
        }
        this.socket.sendPlayerMove(userInfo)
    }

}

好,现在我们就搭建好了一个简单的前端的登入界面,和注册界面,这里,使用了一个简单的code标识,便于服务端进行判断,该请求到底要转发到哪里的。如,10001:登入,10002:注册等等,一开始先这样测试一下,后面再专门同一管理。

下面开始搭建服务端的代码:

目录 

1.创建一个简单的SpringBoot项目,

2.异步启动netty

3.定义一个接收消息管理器,处理消息

4.定义一个消息分发处理器,处理不同的code,进行分发到不同的对象进行处理

5.定义一个通道类,用来保存通道绑定该玩家相关的信息。

6.定义一个通道管理类,用来管理全部登入的玩家,和删除已经退出的玩家。

7.定义一些对于的枚举类,和一些工具类

1.创建一个简单的SpringBoot项目,

相关的依赖,这网络上很多,就不多讲了

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>4.1.66.Final</version> <!-- 使用最新版本 -->
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>

        <!--mysql-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.18</version>
        </dependency>
        <!-- mybatis -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.3</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.76</version>

        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
2.异步启动netty

这是SpringBoot 的启动类,我们异步启动netty

@Async
@EnableAsync
@SpringBootApplication
public class WebsocketTwoApplication  implements CommandLineRunner {

    public static void main(String[] args) {
        SpringApplication.run(WebsocketTwoApplication.class, args);
    }

    @Override
    public void run(String... args) throws Exception {
       new MyNettyWebSocketApplication().bind();
    }
}

这是netty启动类,网络上有很多,我这个是最简单的,我这里是直接用已有,没有进行对数据的编码和解码,这肯定是不行,但是对于登入和注册界面的问题不大,就先暂时先这样了,这里固定了监听窗口,8082,

public class MyNettyWebSocketApplication {

        public void bind() throws InterruptedException {
            EventLoopGroup bossGroup = new NioEventLoopGroup();
            EventLoopGroup workerGroup = new NioEventLoopGroup();
            try {
                ServerBootstrap b = new ServerBootstrap();
                b.group(bossGroup, workerGroup)
                        .channel(NioServerSocketChannel.class)
                        .childHandler(new ChannelInitializer<SocketChannel>() {
                            @Override
                            public void initChannel(SocketChannel ch) throws Exception {
                                ChannelPipeline pipeline = ch.pipeline();
                                pipeline.addLast(new HttpServerCodec()); // HTTP 协议解析,用于握手阶段
                                pipeline.addLast(new HttpObjectAggregator(65536)); // HTTP 协议解析,用于握手阶段
                                pipeline.addLast(new WebSocketServerCompressionHandler()); // WebSocket 数据压缩扩展
                                pipeline.addLast(new WebSocketServerProtocolHandler("/",null  , true)); // WebSocket 握手、控制帧处理
                                pipeline.addLast(new JsonDecoder());
                                pipeline.addLast(new MyWebSocketServerHandler());
                            }
                        });
                ChannelFuture f = b.bind(8082).sync();
                f.channel().closeFuture().sync();
            } finally {
                workerGroup.shutdownGracefully();
                bossGroup.shutdownGracefully();
            }
        }
}

3.定义一个接收消息管理器,处理消息

这里就是netty的一个消息处理器,channelReal0 就是当接收到数据的时候调用的方法,

下面的那个channelReal 就是为了不让它自动释放资源而进行重写的方法,本来我这里想的是继承

ChannelInboundHandlerAdapter 这个类就不会自动释放资源,现在继承的类就是这个类的子类,但是不明为什么,当我继承ChannelInboundHandlerAdapter 这个类的时候,它不能后调用我写的解码方法,也就是ByteToMessageDecoder 这个类 重写的方法,所以没办法就只能先将就一下,使用这个simple类,后面找到原因再修改回来

public class MyWebSocketServerHandler extends SimpleChannelInboundHandler<WebSocketFrame> {
    // 数据处理
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, WebSocketFrame frame) throws Exception {
        if (frame instanceof TextWebSocketFrame) {
            String request = ((TextWebSocketFrame) frame).text();
            manager.getInstance().RspClienMsg(ctx,request);
        }

    }

    //不用释放资源 除非用户退出
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        this.channelRead0(ctx, (WebSocketFrame)msg);
    }





//    @Override
//    public void channelActive(ChannelHandlerContext ctx) throws Exception {
//        super.channelActive(ctx);
//        ctx.read();
//    }
//
//    @Override
//    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        super.channelRead(ctx, msg);
//        if (msg instanceof JsonDecoder){
//            TextWebSocketFrame text  = (TextWebSocketFrame)msg;
//            String request = text.text();
//            manager.getInstance().sendClienMsg(ctx,msg.toString());
//        }
//    }
}
4.定义一个消息分发处理器,处理不同的code,进行分发到不同的对象进行处理

这个manger 类就是用来处理,我们消息的分发,这里我们定义了一个协议

public enum NetCode {
    Non(0), // 错误
    userLogin(1001), // 用户登入

    USERREGISTERED(1002);

    int code;

    NetCode(int code){
        this.code = code;
    }

    public int getCode(){
        return code;
    }

    public static NetCode getType(int code){
        NetCode[] values = NetCode.values();
        for (int i = 0; i < values.length; i++) {
            if(code == values[i].code){
                return values[i];
            }
        }
        return Non;
    }
}

 我们根据请求里面的code来进行请求的分发。

public class manager {

    private static manager instance;
    @Autowired
    NetConnManager netConnManager;
    static UserService userService;
    public manager() {
        userService = SpringBeanUtil.getBean(UserService.class);
    }
    public static  manager getInstance() {
        if (instance == null){
            return  instance = new  manager();
        }
        return instance;
    }

    // 分发消息
    public  void  RspClienMsg(ChannelHandlerContext ctx, String msg){

        NetMessage message = JSON.parseObject(msg, NetMessage.class);
        NetConn conn = getNetConn(ctx);
        switch (NetCode.getType(message.code)){
            case Non:
                System.out.println("无效请求头");
                ctx.channel().writeAndFlush(new TextWebSocketFrame( "无效请求头"));
                break;
            case userLogin:
//                ctx.channel().writeAndFlush(new TextWebSocketFrame( "用户登入"));
                userService.UserLogin(conn,message);
                break;
            case USERREGISTERED:
//                ctx.channel().writeAndFlush(new TextWebSocketFrame( "用户登入"));
                userService.registered(conn,message);
                break;
        }
    }

    NetConn getNetConn(ChannelHandlerContext ctx){

        NetConn conn = NetConnManager.getNetConn(ctx);
        if (conn == null){
            conn = new NetConn(ctx);
        }
        return  conn;
    }


}

5.定义一个通道类,用来保存通道绑定该玩家相关的信息。

这里我们定义一个类来进行通道和用户之间的绑定,只有我们再登入的时候才会进行这个用户的绑定,这样我们就能找到用户进行对应的业务操作

public class NetConn {

    public Player player;

    ChannelHandlerContext ctx;

    public NetConn(ChannelHandlerContext ctx) {
        this.ctx = ctx;
    }

    public void send(String msg) {
        if (ctx != null) {
            ctx.channel().writeAndFlush(new TextWebSocketFrame(msg));
        }
    }


}
6.定义一个通道管理类,用来管理全部登入的玩家,和删除已经退出的玩家。

刚刚我们定义了一个绑定的类,现在我们定义一个管理这个绑定的管理类,来进行查找对应的通道,判断用户的在线,以及将所用登入的用户都添加近我们的players里面,这样就有利于,我们后面进行的匹配,

public class NetConnManager {

    static Map<ChannelHandlerContext, NetConn> ctxs = new HashMap<>();

    static Map<Integer, NetConn> players = new HashMap<>();

    public static NetConn getNetConn(ChannelHandlerContext ctx) {
        return ctxs.get(ctx);
    }

    public static NetConn getNetConn(int id) {
        return players.get(id);
    }

    public static void addNetConn(NetConn conn) {
        ctxs.put(conn.ctx, conn);
        players.put(conn.player.id, conn);
    }


    public static void remove(Player player){
        NetConn netConn = players.remove(player.id);
        if(netConn != null){
            ctxs.remove(netConn.ctx);
        }
    }


}
7.定义一些对于的枚举类,和一些工具类

 这是一个spring的工具类用来获取对应的bean

@Component
public class SpringBeanUtil implements ApplicationContextAware {

    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        if (SpringBeanUtil.applicationContext == null) {
            SpringBeanUtil.applicationContext = applicationContext;
        }
    }

    //获取applicationContext
    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }

    //通过name获取 Bean.
    public static Object getBean(String name) {
        return getApplicationContext().getBean(name);
    }

    //通过class获取Bean.
    public static <T> T getBean(Class<T> clazz) {
        return getApplicationContext().getBean(clazz);
    }

    //通过name,以及Clazz返回指定的Bean
    public static <T> T getBean(String name, Class<T> clazz) {
        return getApplicationContext().getBean(name, clazz);
    }

}


让后就是写 service 和dao 啦,这个就很简单的逻辑啦,这里,就不放代码了,有需要的朋友,我再发出来,简单说一下逻辑,登入,就是查询,用户,判断密码。注册,就是直接insert,就好啦,

数据库,就是很简单一个player表,id,username,password,nickname;

好啦就到这里啦,后面再慢慢完善,写这篇博客就是怕直接这个拖延,弄得一个项目要搞很久,来提醒一下自己,当然上面其实还有很多不懂的地方,希望大家多多指教

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值