Giter Club home page Giter Club logo

eunet's Introduction

EuNet C# (.NET, .NET Core, Unity)

GitHub Actions GitHub Actions nuget Releases

Easy Unity Network (EuNet) is a network solution for multiplayer games.

Supports Server-Client, Peer to Peer communication using TCP, UDP, and RUDP protocols.

In the case of P2P (Peer to Peer), supports hole punching and tries to communicate directly as much as possible, and if it is impossible, automatically relayed through the server.

Great for developing Action MORPG, MOBA, Channel Based MMORPG, Casual Multiplayer Game (e.g. League of Legends, Among Us, Kart Rider, Diablo, etc.).

Produced based on .Net Standard 2.0, multiplatform supported(Windows, Linux, Android, iOS, etc.), and is optimized for .Net Core-based servers and Unity3D-based clients.

RPC(Remote procedure call) can be used to call remote functions and receive return values.
There is no overhead as it serializes at high speed and calls remote functions.
Work efficiency increases as there is no work to create a message every time.

Example

EuNet-Starter EuNet-Tanks
https://github.com/zestylife/EuNet-Starter https://github.com/zestylife/EuNet-Tanks
Google Play
image image

Diagram

image

Table of Contents

Features

  • Fast network communication
    • High speed communication using multi-thread
    • Fast allocation using pooling buffer
  • Supported channels
    • TCP
    • Unreliable UDP
    • Reliable Ordered UDP
    • Reliable Unordered UDP
    • Reliable Sequenced UDP
    • Sequenced UDP
  • Supported communication
    • Client to Server
    • Peer to Peer
      • Hole Punching
      • Relay (Auto Switching)
  • RPC (Remote Procedure Call)
  • Fast packet serializer (Partial using MessagePack for C#)
  • Custom Compiler(EuNetCodeGenerator) for fast serializing and RPC
  • Automatic MTU detection
  • Automatic fragmentation of large UDP packets
  • Automatic merging small packets
  • Unity3D support
  • Supported platforms
    • Windows / Mac / Linux (.Net Core)
    • Android (Unity)
    • iOS (Unity)

Channels

Channels Transmission guarantee Not duplicate Order guarantee
TCP ✔️ ✔️ ✔️
Unreliable UDP
Reliable Ordered UDP ✔️ ✔️ ✔️
Reliable Unordered UDP ✔️ ✔️
Reliable Sequenced UDP ✔️(Last order) ✔️ ✔️
Sequenced UDP ✔️ ✔️

Installation

We need three projects

  • Common project (.Net Standard 2.0)
    • Server, Client common use
    • Generate code using EuNetCodeGenerator
  • Server project (.Net Core)
  • Client project (Unity3D)

See EuNet-Starter for an example

Common project

  • Create .Net Standard 2.0 based project.
  • Install nuget package.
PM> Install-Package EuNet.CodeGenerator.Templates
  • Rebuild project.
  • If you look at the project, CodeGen/EuNet.Rpc.CodeGen.cs file was created.

Server project (.net core)

  • First install the nuget package.
PM> Install-Package EuNet
  • Add common project to reference
Solution Explorer -> [User Project] -> References -> Add Reference -> [Add Common project]

Client project (Unity3D)

Install via git URL

After Unity 2019.3.4f1, Unity 2020.1a21, that support path query parameter of git package. You can add package from UPM (Unity Package Manager)

https://github.com/zestylife/EuNet.git?path=src/EuNet.Unity/Assets/Plugins/EuNet

If you want to add a specific release version, add #version after the url. ex) version 1.1.13

https://github.com/zestylife/EuNet.git?path=src/EuNet.Unity/Assets/Plugins/EuNet#1.1.13

image

Install via package file

RPC (Remote procedure call)

RPC(Remote procedure call) can be used to call remote functions and receive return values.
There is no overhead as it serializes at high speed and calls remote functions.
Work efficiency increases as there is no work to create a message every time.

image

Common project

// Declaring login rpc interface
public interface ILoginRpc : IRpc
{
    Task<int> Login(string id, ISession session);
    Task<UserInfo> GetUserInfo();
}
// Generate Rpc code using EuNetCodeGenerator and use it in server and client

Server project (.Net Core)

// User session class inherits Rpc Interface (ILoginRpc)
public partial class UserSession : ILoginRpc
{
    private UserInfo _userInfo = new UserInfo();
    
    // Implement Rpc Method that client calls
    public Task<int> Login(string id, ISession session)
    {
        if (id == "AuthedId")
            return Task<int>.FromResult(0);

        return Task<int>.FromResult(1);
    }

    // Implement Rpc Method that client calls
    public Task<UserInfo> GetUserInfo()
    {
        // Set user information
        _userInfo.Name = "abc";

        return Task<UserInfo>.FromResult(_userInfo);
    }
}

Client project (Unity3D)

private async UniTaskVoid ConnectAsync()
{
    var client = NetClientGlobal.Instance.Client;

    // Trying to connect. Timeout is 10 seconds.
    var result = await client.ConnectAsync(TimeSpan.FromSeconds(10));

    if(result == true)
    {
        // Create an object for calling login Rpc
        LoginRpc loginRpc = new LoginRpc(client);

        // Call the server's login function (UserSession.Login)
        var loginResult = await loginRpc.Login("AuthedId", null);

        Debug.Log($"Login Result : {loginResult}");
        if (loginResult != 0)
            return;
        
        // Call the server's get user information function (UserSession.GetUserInfo)
        var userInfo = await loginRpc.GetUserInfo();
        Debug.Log($"UserName : {userInfo.Name}");
        // UserName : abc
    }
    else
    {
        // Fail to connect
        Debug.LogError("Fail to connect server");
    }
}

Quick Start

Serialize

Object serialization is required to use Rpc. There are two ways to serialize objects.

Using Auto-Generated formmater

Declaring the NetDataObject Attribute makes the class serializable. All public objects are serialized. Declaring the [IgnoreMember] Attribute does not serialize it.

[NetDataObject]
public class DataClass
{
    // Serializable
    public int Int;

    // Serializable
    public int Property { get; set; }

    // Ignore
    public int PropertyOnlyGet { get; }

    // Ignore
    private int IntPrivate;

    // Ignore
    protected int IntProtected;
    
    // Ignore
    [IgnoreMember]
    public int IgnoreInt;

    // Ignore
    [IgnoreMember]
    public int IgnoreProperty { get; set; }
}

Manualy serialize

Implement serialization manually by inheriting INetSerializable. You have to code, but it's the fastest and most flexible.

public class InterfaceSerializeClass : INetSerializable
{
    public int Value;
    public string Name;

    public void Serialize(NetDataWriter writer)
    {
        writer.Write(Value);
        writer.Write(Name);
    }

    public void Deserialize(NetDataReader reader)
    {
        Value = reader.ReadInt32();
        Name = reader.ReadString();
    }
}

Unity3D

  • Special object NetView is supported, and synchronization and Rpc communication between NetViews are possible.
    (NetClientP2pBehaviour required. Peer to Peer only)
  • Supported global settings with NetClientGlobal object. (Singleton)
  • Supported for communication to the server (NetClientBehaviour, NetClientP2pBehaviour)
  • Support NetClientP2pBehaviour only one.

Settings

  • Add an empty GameObject to the first run Scene and add a NetClientGlobal component.
    Only one should be made globally.

image

  • Add NetClientP2pBehaviour component for communicate to one server (including P2p).
    Modify the options as needed.

image

  • Add user component to receive and process events.

image

  • GameClient.cs file
using Common.Resolvers;
using EuNet.Core;
using EuNet.Unity;
using System.Threading.Tasks;

public class GameClient : Singleton<GameClient>
{
    private NetClientP2pBehaviour _client;

    public NetClientP2p Client => _client.ClientP2p;

    protected override void Awake()
    {
        base.Awake();

        _client = GetComponent<NetClientP2pBehaviour>();

        Client.OnConnected = OnConnected;
        Client.OnClosed = OnClosed;
        Client.OnReceived = OnReceive;
        
        // Register automatically generated resolver.
        //CustomResolver.Register(GeneratedResolver.Instance);

        // If you generated RpcService, register it.
        //Client.AddRpcService(new GameScRpcService());
    }

    public Task<bool> ConnectAsync()
    {
        // Try to connect server. All functions can be accessed with Client instance
        return Client.ConnectAsync(TimeSpan.FromSeconds(10));
    }

    private void OnConnected()
    {
        // Connected
    }

    private void OnClosed()
    {
        // Disconnected

    private Task OnReceive(NetDataReader reader)
    {
        // Received data. No need to use when using RPC
        return Task.CompletedTask;
    }
}

How to use Rpc

Rpc is service that can call remote procedures.
EuNet's Rpc is a function call service between the server and the client.
When you declare an interface that inherits the IRpc interface, calls and service codes are automatically generated.

  • Create Rpc Interface in Common project.
  • Build project.
using EuNet.Rpc;
using System.Threading.Tasks;

namespace Common
{
    // Inherit IRpc for Rpc
    public interface IGameCsRpc : IRpc
    {
        // Login Rpc
        Task<int> Login(string id);
    }
}
  • In the Server project, register RpcService when creating a server .
_server.AddRpcService(new GameCsRpcServiceSession());
  • In the Server project, UserSession class inherits from IGameCsRpc.
public partial class UserSession : IGameCsRpc
{
    public Task<int> Login(string id)
    {
        return Task.FromResult(0);
    }
}
  • In the Client project, call Rpc.
// Rpc callable object
var rpc = new GameCsRpc(_client.Client, null, TimeSpan.FromSeconds(10));

// Call Rpc Login
var loginResult = await rpc.Login("MyId");
Debug.Log(loginResult);

How to use ViewRpc

ViewRpc is a technology that makes peer-to-peer communication between NetView Components as Rpc.
By adding a NetView Component to the GameObject, you can call functions of the same NetView Component (same ViewId) that exist on different clients.
For example, if you shoot a cannon from a red tank, the other user's red tank will also fire.
1:1 or 1:N call is possible, and in case of 1:N, return value can not be received.

IL2CPP issue (AOT)

Some platforms do not allow runtime code generation. Therefore, any managed code which depends upon just-in-time (JIT) compilation on the target device will fail. Instead, you need to compile all of the managed code ahead-of-time (AOT). Often, this distinction doesn’t matter, but in a few specific cases, AOT platforms require additional consideration.

See more
https://docs.unity3d.com/2019.4/Documentation/Manual/ScriptingRestrictions.html

Serialization

There is a problem when serializing generic objects as AOT cannot generate code So, you need to provide a hint so that AOT can generate the code.

  • Class for serialize (In Common project)
[NetDataObject]
public class DataClass
{
    public Tuple<int,string> TupleData;
    public Dictionary<int,string> DictionaryData;
}
  • Hint function (In Client unity project)
private void UsedOnlyForAOTCodeGeneration()
{
    // Hints for using <int,string> in TupleFormatter<T,T>
    new TupleFormatter<int, string>();

    // Hints for using <int,string> in DictionaryFormatter<T,T>
    new DictionaryFormatter<int, string>();

    // Exception!
    throw new InvalidOperationException("This method is used for AOT code generation only. Do not call it at runtime.");
}

eunet's People

Contributors

zestylife avatar zestyroad avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

eunet's Issues

Can not send very long string by ppc call

I used p2p call to send string to other p2p.May be because of the string length is too big, an error occurred.
By your design, if the package data is too big, it will be split to small packets and then send to others. But it
do not worked,I don't know why.
`
public async void CommonRpc2Target(string msg, ushort sessionId)
{
if (_view.IsMine() == false)
return;

    await _actorRpc
        .ToTarget(DeliveryMethod.ReliableUnordered, sessionId)
        .OnCommonRpc(msg);
}

`

Please how can I enable the auto package split.

Error:
ArgumentException: Offset and length were out of bounds for the array or count is greater than the number of elements from index to the end of the source collection.
System.Buffer.BlockCopy (System.Array src, System.Int32 srcOffset, System.Array dst, System.Int32 dstOffset, System.Int32 count) (at <695d1cc93cca45069c528c15c9fdd749>:0)
EuNet.Core.UdpChannel.SendTo (System.Byte[] data, System.Int32 offset, System.Int32 size, EuNet.Core.UdpChannel+SendMode sendMode) (at Assets/Plugins/EuNet/Runtime/EuNet.Core/Channel/UdpChannel.cs:580)
EuNet.Core.ReliableSendInfo.TrySend (System.Int64 currentTime, System.Int32 disconnectTimeoutMs, EuNet.Core.UdpChannel udpChannel, EuNet.Core.NetStatistic statistic) (at Assets/Plugins/EuNet/Runtime/EuNet.Core/Channel/ReliableSendInfo.cs:64)
EuNet.Core.ReliableChannel.SendPendingPacket () (at Assets/Plugins/EuNet/Runtime/EuNet.Core/Channel/ReliableChannel.cs:201)
EuNet.Core.ReliableChannel.Update (System.Int32 elapsedTime) (at Assets/Plugins/EuNet/Runtime/EuNet.Core/Channel/ReliableChannel.cs:128)
EuNet.Core.UdpChannel.Update (System.Int32 elapsedTime) (at Assets/Plugins/EuNet/Runtime/EuNet.Core/Channel/UdpChannel.cs:286)
EuNet.Client.NetClient.Update (System.Int32 elapsedTime) (at Assets/Plugins/EuNet/Runtime/EuNet.Client/NetClient.cs:288)
UnityEngine.Debug:LogException(Exception)
EuNet.Unity.NetClientBehaviour:OnError(Exception) (at Assets/Plugins/EuNet/Runtime/EuNet.Unity/NetClientBehaviour.cs:105)
EuNet.Client.NetClient:OnError(Exception) (at Assets/Plugins/EuNet/Runtime/EuNet.Client/NetClient.cs:382)
EuNet.Client.NetClient:Update(Int32) (at Assets/Plugins/EuNet/Runtime/EuNet.Client/NetClient.cs:340)
EuNet.Unity.NetClientP2p:FixedUpdate(Single) (at Assets/Plugins/EuNet/Runtime/EuNet.Unity/Client/NetClientP2p.cs:58)
EuNet.Unity.NetClientP2pBehaviour:FixedUpdate() (at Assets/Plugins/EuNet/Runtime/EuNet.Unity/NetClientP2pBehaviour.cs:79)

CodeGenerator

Is there a way to run CodeGenerator in order to add new lines of code to exicting CodeGenerator script since it's not recommended to modify this code manually?

unity client compile error - AsyncObjectQueue does not contain a definition for Clear - _queue.Clear();

when you try to compile one of the demo projects (EuNet-Starter or EuNet-Tanks) appears the error.
only when you compile in unity not when only play

Library\PackageCache\com.zestylife.eunet@f63a536\Runtime\EuNet.Core\Util\AsyncObjectQueue.cs(20,20): error CS1061: 'ConcurrentQueue' does not contain a definition for 'Clear' and no accessible extension method 'Clear' accepting a first argument of type 'ConcurrentQueue' could be found (are you missing a using directive or an assembly reference?)

in the class AsyncObjectQueue, _queue.Clear(); give an error

QuickJoinRoom

Hi, very nice project.
Is it possibile to join room with the roomCode instead of using the QuickJoinRoom method?

Thx

NullReferenceException on P2pTest.cs / P2pTest에서 오류 발생

안녕하세요, 개발자가 한국인이길래 이슈도 한국어로 해도 괜찮은거겠죠?

아무튼 P2pTest.cs 에서 테스트를 해보면
image
이렇게 정상적으로 테스트가 성공되지만,

제가 직접 코드를 복붙해보고 저의 프로젝트에 적용해보면 (WPF, Net Framework 4.8)
image
TcpListener.cs에서 NullReferenceException 오류가 나면서 P2P 테스트가 되질 않습니다.

참고로 DLL은 EuNet 프로젝트를 빌드하고 복사해서 프로젝트에 적용했습니다.

코드는 Assert 부분 빼고 완벽하게 복붙해서 코드 부분은 걱정하지 않아도 될 것 같습니다.
혹시 모르니 코드 부분도 첨부하도록 하겠습니다.

private P2pGroup _p2pGroup;

        public async Task Test(
            int clientCount,
            int sendCount)
        {
            var serverTcs = new TaskCompletionSource<string>();
            int receivedUnreliableCount = 0;
            int receivedTcpCount = 0;

            NetServer server = new NetServer(
                new ServerOption()
                {
                    Name = "TestServer",
                    TcpServerPort = 9000,
                    IsServiceUdp = true,
                    UdpServerPort = 9001,
                    TcpBackLog = Math.Max(clientCount, 512),
                    MaxSession = clientCount,
                });

            server.OnSessionReceived += (ISession session, NetDataReader reader) =>
            {
                var text = reader.ReadString();

                var writer = NetPool.DataWriterPool.Alloc();
                try
                {
                    if (text == "Join")
                    {
                        var result = _p2pGroup.Join(session as ServerSession);

                        Console.WriteLine($"P2p joined {result}");
                    }
                    else if (text == "Leave")
                    {
                        var result = _p2pGroup.Leave(session as ServerSession);

                        Console.WriteLine($"P2p leaved {result}");
                    }
                    else if (text == "Unreliable")
                    {
                        Interlocked.Increment(ref receivedUnreliableCount);

                        writer.Write("Unreliable");

                        session.SendAsync(writer, DeliveryMethod.Unreliable);
                    }
                    else
                    {
                        Interlocked.Increment(ref receivedTcpCount);

                        if (text == "Finish")
                        {
                            writer.Write("Finish");
                        }
                        else writer.Write($"Hello Client{session.SessionId}");

                        session.SendAsync(writer, DeliveryMethod.Tcp);
                    }
                }
                finally
                {
                    NetPool.DataWriterPool.Free(writer);
                }

                return Task.CompletedTask;
            };

            server.OnSessionErrored += (ISession session, Exception ex) =>
            {
                serverTcs.TrySetException(ex);
            };

            await server.StartAsync();

            _p2pGroup = server.P2pManager.CreateP2pGroup();

            List<Task<NetClient>> taskList = new List<Task<NetClient>>();
            for (int i = 0; i < clientCount; i++)
            {
                taskList.Add(WorkClient(i, sendCount));
            }

            await Task.WhenAny(Task.WhenAll(taskList), serverTcs.Task);

            foreach (var task in taskList)
                task.Result.Close();

            await Task.Delay(1000);

            await server.StopAsync();

            await Task.Delay(1000);

            Console.WriteLine($"Server receivedTcpCount : {receivedTcpCount}");
            Console.WriteLine($"Server receivedUnreliableCount : {receivedUnreliableCount}");

            //NetPool.PacketPool은 internal 속성이므로 제외

            Console.WriteLine("****** DataWriterPool ******");
            Console.WriteLine(NetPool.DataWriterPool.ToString());
            Console.WriteLine("");
        }

        private async Task<NetClient> WorkClient(int index, int sendCount)
        {
            NetClient client = new NetClient(new ClientOption()
            {
                TcpServerAddress = "127.0.0.1",
                TcpServerPort = 9000,
                IsServiceUdp = true,
                UdpServerAddress = "127.0.0.1",
                UdpServerPort = 9001
            });

            int receivedUnreliableCount = 0;
            int receivedP2pCount = 0;
            var tcs = new TaskCompletionSource<string>();

            client.OnReceived += (NetDataReader reader) =>
            {
                string text = reader.ReadString();

                if (text == "Finish")
                    tcs.SetResult(text);
                else if (text == "Unreliable")
                {
                    receivedUnreliableCount++;
                }
                else
                {

                }

                return Task.CompletedTask;
            };

            client.OnP2pReceived += (ISession session, NetDataReader reader) =>
            {
                string text = reader.ReadString();
                //Console.WriteLine($"------------- {text}");
                receivedP2pCount++;

                return Task.CompletedTask;
            };

            client.OnErrored += (e) =>
            {
                tcs.SetException(e);
            };

            client.OnClosed += () =>
            {
                //tcs.SetException(new Exception("session closed"));
                tcs.TrySetException(new Exception("session closed"));
            };

            Task.Factory.StartNew(async () =>
            {
                await Task.Delay(100);
                Stopwatch sw = Stopwatch.StartNew();
                while (client.State != SessionState.Closed)
                {
                    sw.Stop();
                    client.Update((int)sw.ElapsedMilliseconds);
                    sw.Restart();
                    await Task.Delay(30);
                }
            }).DoNotAwait();

            var connected = await client.ConnectAsync(null); //여기서 NullReferenceException 발생

            var writer = NetPool.DataWriterPool.Alloc();
            try
            {
                writer.Write("Join");
                client.SendAsync(writer, DeliveryMethod.Tcp);
            }
            finally
            {
                NetPool.DataWriterPool.Free(writer);
            }

            await Task.Delay(3000);

            writer = NetPool.DataWriterPool.Alloc();
            try
            {
                writer.Write($"P2pReliableOrdered {client.SessionId}");
                client.P2pGroup.SendAll(writer, DeliveryMethod.ReliableOrdered);
            }
            finally
            {
                NetPool.DataWriterPool.Free(writer);
            }

            writer = NetPool.DataWriterPool.Alloc();
            try
            {
                writer.Write($"P2pUnreliable {client.SessionId}");
                client.P2pGroup.SendAll(writer, DeliveryMethod.Unreliable);
            }
            finally
            {
                NetPool.DataWriterPool.Free(writer);
            }

            await Task.Delay(1000);

            if (index % 10 != 1)
            {
                writer = NetPool.DataWriterPool.Alloc();
                try
                {
                    writer.Write("Leave");
                    client.SendAsync(writer, DeliveryMethod.Tcp);
                }
                finally
                {
                    NetPool.DataWriterPool.Free(writer);
                }

                await Task.Delay(1000);
            }

            writer = NetPool.DataWriterPool.Alloc();
            try
            {
                writer.Write("Finish");
                client.SendAsync(writer, DeliveryMethod.Tcp);
            }
            finally
            {
                NetPool.DataWriterPool.Free(writer);
            }

            /*for (int i = 0; i < sendCount; i++)
            {
                writer = NetPool.DataWriterPool.Alloc();
                try
                {
                    if (i < sendCount - 1)
                        writer.Put("Hello Server");
                    else writer.Put("Finish");

                    client.SendAsync(writer, DeliveryMethod.Tcp);
                }
                finally
                {
                    NetPool.DataWriterPool.Free(writer);
                }

                writer = NetPool.DataWriterPool.Alloc();
                try
                {
                    writer.Put("Unreliable");

                    client.SendAsync(writer, DeliveryMethod.Unreliable);
                }
                finally
                {
                    NetPool.DataWriterPool.Free(writer);
                }
            }*/

            await Task.Delay(1000);

            await tcs.Task;

            Console.WriteLine($"receivedUnreliableCount : {receivedUnreliableCount}");

            return client;
        }

        private async void xProdigy_Click(object sender, RoutedEventArgs e)
        {
            await Test(3, 1);
            MessageBox.Show("hurary!");
        }

감사합니다.

PERFORMANCE TEST

Hi
I'have made a project on your fantastic system.
I'm currently testing it, in your opinion, how many player can stay in a room at the same time.
I'm testing 20 players in the same room (19 of them in my computer and 1 on my smartphone a Samusung A20e because it will be a mobile game).
The game is very slow and the frame rate is very low. In your opinion should it be normal?

Thanks,
Lorenzo

NetDataObjectAttribute issue (?)

I don't know if I did everything right, but if you try to generate code using scripts with NetDatObject attribute, the codegenerator will not find this attribute so that source code will be ignored during the generation process. I used source codes from your EuNet-Starter project: IShopRpc.cs, ILoginRpc.cs IActorViewRpc.cs and UserUnfo.cs.
On the screenshot below you can see that only 3 out of 4 source code are generated.

cmd

As far as I understand this issue, the problem might be in the Utility.cs in IsNetDataObjectAttribute method, because attr variable never gets any value from GetCustomAttribute, except null.

NetDataObjectAttribute

Can you please tell me is this a script issue or I just to dumb? 😄

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.