1

Тема: Пишемо онлайн гру на Unity з власним сервером на Java

Це не повний тутор і не повна статя, а тема, в котрій я буду хвалитись тим, що роблю, і жалітись, якщо щось не виходе.
Справа така, що я зараз пишу онлайн гру, але вже за гроші. Це не якась ммо, а turn-based пошагова стратегія (чи тактекія, не знаю, що тут краще примінити - стратегія, чи тактика).
В грі буде реєстрація з логіном, тому потрібна база даних, і все таке.
Пароль буде генеруватись на стороні серверу і відправлятись користувачу на мило, тому усіляке php йде котові під хвіст.

Поки що мені потрібно зробити лише реєстрацію і логін, і я почав з наймешного:
1. Створити найпростіший сервер, котрий би просто приймав підключення і дані від клієнтів
2. Створити клієнта, спроможного підключитись до серверу та відправити дані

Я зайшов на півкроку далі, і відправляю отримані від клієнта дані назад до клієнта.

В якості мережевої бібліотеки на стороні серверу використовую Netty.

Спершу нехай буде код серверу, бо він страшний.
Я розумію далеко не кожний рядок, тому що лише переписував з офіційного туторіалу намагаючись розібратись, але чим далі, тим зрозуміліше буде, як то все працює.

Озьдо туторіал
http://netty.io/wiki/user-guide-for-4.x.html

озьдо клас обробник
дані приходяться в метод channelRead, отримані дані виводяться в консоль, а потім відправляються клієнту

import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
import io.netty.buffer.ByteBuf;

public class DiscardServerHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg){
        ByteBuf msg1 = (ByteBuf)msg;
        System.out.println(msg1.toString(io.netty.util.CharsetUtil.US_ASCII));
        
        ctx.writeAndFlush(msg1);
    }
    
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause){
        cause.printStackTrace();
        ctx.close();
    }
}

озьдо клас, де сервер збирається до купи

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;

public class DiscardServer {

    private int port;
    
    public DiscardServer(int port)
    {
        this.port=port;
    }
    
    public void run() throws Exception {
        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 {
                    ch.pipeline().addLast(new DiscardServerHandler());
                }
            })
            .option(ChannelOption.SO_BACKLOG, 128)
            .childOption(ChannelOption.SO_KEEPALIVE, true);
            
            ChannelFuture f = b.bind(port).sync();
            
            f.channel().closeFuture().sync();
        }
        finally{
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }
    
}

а озьдо туто воно все запускається

public class Main {
    
    public static void main(String[] args) throws Exception
    {
        int port;
        if(args.length>0){
            port = Integer.parseInt(args[0]);
        }
        else{
            port = 8050;
        }
        new DiscardServer(port).run();
        
        System.out.println("end");
    }
}

Як бачите, сервер слухаю 8050 порт.

А от скрипт на клієнті

using UnityEngine;
using System.Collections;
using System.Net.Sockets;
using System;
using System.Text;

public class TacticsNet : MonoBehaviour {

    private TcpClient client;

    void Awake()
    {
        client = new TcpClient();
    }

    void Start()
    {
        Connect();        
    }

    void Connect()
    {
        client.Connect("localhost", 8050);
        Debug.Log("Connected");

        Send();
    }

    void Send()
    {
        byte[] data = Encoding.UTF8.GetBytes("hello server, glad to feel you <3");
        try {
            client.GetStream().Write(data, 0, data.Length);
        }
        catch(Exception ex)
        {
            Debug.LogError(ex.Message);
        }

        Debug.Log("Data has been sent");

        Receive();        
    }

    void Receive()
    {
        byte[] data = new byte[1024];
        int dataLength = client.GetStream().Read(data, 0, data.Length);
        string message = Encoding.UTF8.GetString(data, 0, dataLength);

        Debug.Log(message);

        Disconnect();
    }

    void Disconnect()
    {
        client.Close();
        Debug.Log("Disconnected");
    }
}

Підключаємось, відправляємо, приймаємо. Все в одному потоці, тому й так просто виглядає.

І от скрін роботи
зліва клієнт, справа сервер
http://puu.sh/p91Me/84d7ee5fe3.png

Говоріть українською! Живіть українською! Відчувайте українською!
Подякували: leofun01, Regen2

2

Re: Пишемо онлайн гру на Unity з власним сервером на Java

Я тут розібрався, як ото відправляти кастомні дані до клієнта.
Змінив трохи код на сервері

import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.buffer.ByteBuf;

public class ServerHandler extends ChannelInboundHandlerAdapter {
    
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("we have got a new client connected...");
        System.out.println("ip: "+ctx.channel().remoteAddress());
    };
    
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg){
        ByteBuf msg1 = (ByteBuf)msg;
        System.out.println(msg1.toString(io.netty.util.CharsetUtil.US_ASCII));
        ByteBuf otherBuff = ctx.alloc().buffer();
        otherBuff.writeBytes("i'm glad to feel you too, dear client <3".getBytes());
        msg1.release();
        
        ChannelFuture future0 = ctx.writeAndFlush(otherBuff);
        
        future0.addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture arg0) throws Exception {
                // TODO Auto-generated method stub
                System.out.println("sent");
                
                ChannelFuture future = ctx.close();
                
                future.addListener(new ChannelFutureListener() {
                    @Override
                    public void operationComplete(ChannelFuture arg0) throws Exception {
                        System.out.println("closed");
                    }
                });
            }
        });        
    }
    
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause){
        cause.printStackTrace();
        ctx.close();
    }
}

як вияснилось, усі операції в netty - асинхронні, тому якщо ви відправляєте повідомлення і після цього відключаєте клієнта, то може статись, що клієнта відключать не відправляючи повідомлення, тому тре робити, підключати лістенерів до таких операцій, котрі забезпечують виконання подальших операцій після виконання основної.
І ще я додав обробник підключень, хтів ще вивести айпішник, але він тут якийсь такий калічний... Хоча, для порівння і такий формат має згодитись, тільки порт треба відкинути
http://puu.sh/p934r/1942b96971.png

Говоріть українською! Живіть українською! Відчувайте українською!
Подякували: Regen1

3

Re: Пишемо онлайн гру на Unity з власним сервером на Java

Вирішив побороти фрагментацію, прочитав про це в туторіалі, глянув документацію по класу, котрий спеціалізується на цій проблемі і написав отаке

import java.util.List;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ReplayingDecoder;

public class MessageDecoder extends ReplayingDecoder<DecoderState> {

    private int length;

    public MessageDecoder()
    {
        super(DecoderState.READ_LENGTH);
    }

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf buf, List<Object> out) throws Exception{
        System.out.println(buf.readableBytes());
        switch(state()){
            case READ_LENGTH:
                length=buf.readInt();
                System.out.println("length is: "+length);
                checkpoint(DecoderState.READ_CONTENT);
            case READ_CONTENT:
                ByteBuf frame = buf.readBytes(length);
                checkpoint(DecoderState.READ_LENGTH);
                out.add(frame);
                break;
            default:
                throw new Error("Shouldn't reach here");
        }
    }
}

Якщо хтось не розуміє, про що я. То tcp працює в режимі потоку, тобто, якщо ви відправили повідомлення "привіт, сервер", то на сервер може прийти два повідомлення, перше буде "прив", а друге "іт, сервер".
І це необхідно виправити.

Як ви розумієте, розмір контенту може бути різний, але якщо на стороні серверу ми не знаємо, скільки байтів має прийти, то ми ніколи не зможемо вірно обробити ці байти, тому є сенс спочатку відправляти довжину контенту, а довжина - це у нас int, тобто 4 байті.
Тепер ми знаємо, що спочатку приходять 4 байти, котрі містять довжину контенту, а потім вже весь контент.

Ось цей код робить наступним чином.
1. Встановлюємо стан READ_LENGTH, котрий означає, що ми очікуємо довжину контенту
2. Якщо ми в режимі READ_LENGTH і прийшли якісь байти, то намагаємось перевести їх в тип int, якщо у нас недостатньо байтів, 1, 2 чи 3, при потрібних 4, то ми нічо не робимо. В коді цього не написано, тому що частину потрібної роботи робить клас від котрого ми наслідуємось.
3. Якщо у нас є 4 чи більше байтів, то ми переводимо їх в int і отримуємо довжину контенту, котрий прийде в майбутньому, і перемикаємось в режим READ_CONTENT
4. Ну а далі ми просто порівнюємо кількість байтів, що надійшли, з довжиною контенту, що очікується, і як тільки вся потрібна інфа надійшла, тоді ми обробляємо ці байтики.

Ну ви пойняли.

Тільки от зараз в мене вилазить страшна помилка, і я не знаю, як її вирішити.

Говоріть українською! Живіть українською! Відчувайте українською!
Подякували: Regen1

4 Востаннє редагувалося FakiNyan (30.05.2016 14:53:10)

Re: Пишемо онлайн гру на Unity з власним сервером на Java

помилку з дефрагментацією я так і не пофіксив, тому що на стековерфлоу всі мовчать, тому поки що був думав про те, як в Unity викликати методи, що викликаються лише в головному потоці, з іншого потоку.
Гуглив-гуглив, і нагуглив якогось диспетчера, як виявилось, це такий шаблон. Я відразу зрадів, думав - опана! якась нова крута фіча, про котру я не знав! Але все фігня...
Пам'ятаю, два роки тому пан koala розповів мені, що анонімні функції можна зберігати в масиви і т.д., при чому вони зберігаються разом з даними, котрі ви передаєте в момент створення тієї функції, так от той диспетчер - це та сама історія, так що, нічого нового.

Суть того патерна дуже проста - ми створюємо функцію, запихуємо її в масив, а в головному треді перевіряємо той масив на наявність функцій в ньому, якщо щось є, то викликаємо її, таким чином вона викликається в головному треді.

При цьому використовується сінглтон для доступу до диспетчера, але на одному сайті я прочитав, що є кращі варіанти. Один з них - dependency injection, от, зараз про нього буду читати. Тільки чайок поп'ю.

Говоріть українською! Живіть українською! Відчувайте українською!
Подякували: Regen1

5

Re: Пишемо онлайн гру на Unity з власним сервером на Java

Мені дуже сподобалася ваша стаття .
Хоч мені і 12 років я досить добре розбираюся в програмуванні на мові Java .
Якщо б ви могли надіслати копію гри обі був бе дуже радий подивитися на вашу роботу .

6

Re: Пишемо онлайн гру на Unity з власним сервером на Java

я вже забувся, що писав шось таке

Говоріть українською! Живіть українською! Відчувайте українською!