텍스트로 표현된 전송 이벤트는 채팅이 되고, 이를 그래픽화하여 렌더링하고 몇 가지 규칙을 추가하면 게임으로 확장될 수 있습니다.

유니티 같은 게임 엔진을 활용해 노코딩으로도 고퀄리티 3D 게임을 제작할 수 있지만

웹 기반 환경에서 출발하여 브라우저에서 멀티플레이어로 동작할 수 있는 코드를 AI를 이용해 조금씩 디벨롭 했으며

제작 과정에서 사용한 프롬프트및  AI가 생성한 코드, 그리고 실제로 동작하는 전체 코드를 공유합니다.

기술적으로는  웹소켓과 액터 모델을 결합하면 어렵고 복잡할것같은 멀티플레이어 처리를 AI와 협업하여  직관적으로 구현할 수 있는점과

웹환경,웹진영 기술만 이용해 게임에 사용되는 요소를 파악하고 풀스택 멀티플레이어 게임 제작에 도전할수 있는 가능성을 연구해볼수 있습니다.


개발환경


기본베이스


작동서버


링크 :


제작과정중 사용된 프롬프트및 생성코드

hubConnection 에서 커넥트가 최초 성공하면 수신기 만들어죠

protected override async Task OnInitializedAsync()
{
    LoginId = Guid.NewGuid().ToString();    //Fake Login ID

    hubConnection = new HubConnectionBuilder()
        .WithUrl(_navigationManager.ToAbsoluteUri("/chathub"))
        .Build();

    await hubConnection.StartAsync();

    // 수신기 설정
    hubConnection.On<RoomInfo, UserInfo, UpdateUserPos>("OnJoinRoom", (room, user, pos) =>
    {
        Console.WriteLine($"WS - OnJoinRoom");
        if (user.Id == LoginId)
        {
            Name = user.Name;
            RoomName = room.Name;

            SyncRoom syndMsg = new SyncRoom()
            {
                UserInfo = new UserInfo() { Name = "user", Id = LoginId },
                RoomInfo = new RoomInfo() { Name = "room1" }
            };

            hubConnection.SendAsync("SyncRoom", syndMsg).Wait();
        }
        else
        {
            _chatLand.AddUser(user.Id, user.Name, pos.AbsPosX, pos.AbsPosY, false, resource);
        }
    });

    // 다른 수신기 설정
    hubConnection.On<UserInfo, List<UpdateUserPos>>("OnSyncRoom", (user, updateUserPos) =>
    {
        Console.WriteLine($"WS - OnSyncRoom");
        if (user.Id == LoginId)
        {
            foreach (var pos in updateUserPos)
            {
                bool isMine = pos.Id == LoginId ? true : false;

                _chatLand.AddUser(pos.Id, pos.Name, pos.AbsPosX, pos.AbsPosY, isMine, resource);
            }
        }
    });

    //ChatMessage
    hubConnection.On<ChatMessage>("OnChatMessage", (chatMessage) =>
    {
        bool isMine = chatMessage.From.Id == LoginId ? true : false;
        _messages.Add(new Message(chatMessage.From.Name, chatMessage.Message, isMine));
        Console.WriteLine($"WS - OnChatMessage");
        _chatLand.ChatMessage(chatMessage);

        // Inform blazor the UI needs updating
        StateHasChanged();
    });

    hubConnection.On<UpdateUserPos>("OnUpdateUserPos", (userPos) =>
    {
        Console.WriteLine($"WS - OnUpdateUserPos");
        _chatLand.UpdateUserPos(userPos);
    });

    hubConnection.On<LeaveRoom>("OnLeaveRoom", (room) =>
    {
        Console.WriteLine($"WS - OnLeaveRoom");
        _chatLand.RemoveUser(room.UserInfo.Id);

        StateHasChanged();
    });

    JoinRoom sendMsg = new JoinRoom()
    {
        UserInfo = new UserInfo() { Name = "user", Id = LoginId },
        RoomInfo = new RoomInfo() { Name = "room1" }
    };

    await hubConnection.SendAsync("JoInRoom", sendMsg);
}

  ChatMessage 의 History를 큐로 최대 50개까지만 최신기준 유지하고싶습니다. 그리고 이 히스토리를 뷰티 json형태로 반환하는 함수도 만들어

using System.Collections.Concurrent;

namespace BlazorChatApp.Server.Hubs
{
    public class RoomActor : ReceiveActor
    {
        private readonly ILoggingAdapter log = Context.GetLogger();
        public Dictionary<string, UpdateUserPos> users = new Dictionary<string, UpdateUserPos>();
        private string roomName;
        private int userAutoNo = 0;
        private readonly IServiceScopeFactory scopeFactory;
        private readonly ConcurrentQueue<ChatMessage> chatHistory = new ConcurrentQueue<ChatMessage>();
        private const int MaxChatHistoryCount = 50;
        Random random = new Random();

        public RoomActor(string _roomName, IServiceScopeFactory _scopeFactory)
        {
            scopeFactory = _scopeFactory;
            roomName = _roomName;
            log.Info($"Create Room{roomName}");

            Receive<RoomCmd>(cmd => {
                log.Info("Received String message: {0}", cmd);
                //Sender.Tell(message);                
            });

            Receive<JoinRoom>(async cmd => {
                userAutoNo++;
                string jsonString = JsonSerializer.Serialize(cmd);
                log.Info("Received JoinRoom message: {0}", jsonString);
                string RandomColor = string.Format("#{0:X6}", random.Next(0xFFFFFF));

                UserInfo userInfo = new UserInfo()
                {
                    Id = cmd.UserInfo.Id,
                    Name = $"User-{userAutoNo}",
                    Color = RandomColor
                };

                // Default Position
                double posx = random.Next(0, 300);
                double posy = random.Next(400, 550);

                UpdateUserPos updateUserPos = new UpdateUserPos()
                {
                    Id = cmd.UserInfo.Id,
                    Name = $"User-{userAutoNo}",
                    PosX = posx,
                    PosY = posy,
                    AbsPosX = posx,
                    AbsPosY = posy,
                    ConnectionId = cmd.ConnectionId
                };

                if (!users.ContainsKey(updateUserPos.Id))
                    users[cmd.UserInfo.Id] = updateUserPos;

                await OnJoinRoom(cmd.RoomInfo, userInfo, updateUserPos);
            });

            Receive<SyncRoom>(async cmd => {
                string jsonString = JsonSerializer.Serialize(cmd);
                log.Info("Received SyncRoom message: {0}", jsonString);

                List<UpdateUserPos> updateUserPosList = users.Values.ToList();
                string RandomColor = string.Format("#{0:X6}", random.Next(0xFFFFFF));

                UserInfo userInfo = new UserInfo()
                {
                    Id = cmd.UserInfo.Id,
                    Name = $"User-{userAutoNo}",
                    Color = RandomColor
                };

                await OnSyncRoom(userInfo, updateUserPosList);
            });

            Receive<ChatMessage>(async cmd => {
                string jsonString = JsonSerializer.Serialize(cmd);
                log.Info("Received ChatMessage message: {0}", jsonString);

                ChatMessage chatMessage = new ChatMessage()
                {
                    From = cmd.From,
                    Message = cmd.Message
                };

                AddChatMessageToHistory(chatMessage);
                await OnChatMessage(chatMessage);
            });

            Receive<UpdateUserPos>(async cmd => {
                string jsonString = JsonSerializer.Serialize(cmd);
                log.Info("Received UpdateUserPos message: {0}", jsonString);

                double AbsPosX = users[cmd.Id].PosX + cmd.PosX;
                double AbsPosY = users[cmd.Id].PosY + cmd.PosY;

                if (users.ContainsKey(cmd.Id))
                {
                    users[cmd.Id].PosX = AbsPosX;
                    users[cmd.Id].PosY = AbsPosY;
                    users[cmd.Id].AbsPosX = AbsPosX;
                    users[cmd.Id].AbsPosY = AbsPosY;
                }

                log.Info($"UpdateUser : X=>{users[cmd.Id].AbsPosX} Y=>{users[cmd.Id].AbsPosY}");

                UpdateUserPos updateUserPos = new UpdateUserPos()
                {
                    Id = cmd.Id,
                    Name = cmd.Name,
                    PosX = cmd.PosX,
                    PosY = cmd.PosY,
                    AbsPosX = AbsPosX,
                    AbsPosY = AbsPosY
                };

                await OnUpdateUserPos(updateUserPos);
            });

            Receive<Disconnect>(async cmd => {
                string jsonString = JsonSerializer.Serialize(cmd);
                log.Info("Received Disconnect message: {0}", jsonString);
                var disconnectUser = users.Values.Where(e => e.ConnectionId == cmd.ConnectionId).FirstOrDefault();

                if (disconnectUser != null)
                {
                    if (users.ContainsKey(disconnectUser.Id))
                    {
                        users.Remove(disconnectUser.Id);

                        var leaveMsg = new LeaveRoom()
                        {
                            UserInfo = new UserInfo() { Id = disconnectUser.Id }
                        };
                        await OnLeaveRoom(leaveMsg);
                    }
                }
            });

            Receive<LeaveRoom>(async cmd => {
                string jsonString = JsonSerializer.Serialize(cmd);
                log.Info("Received LeaveRoom message: {0}", jsonString);

                if (users.ContainsKey(cmd.UserInfo.Id))
                {
                    users.Remove(cmd.UserInfo.Id);
                    await OnLeaveRoom(cmd);
                }
            });
        }

        private void AddChatMessageToHistory(ChatMessage chatMessage)
        {
            chatHistory.Enqueue(chatMessage);
            while (chatHistory.Count > MaxChatHistoryCount)
            {
                chatHistory.TryDequeue(out _);
            }
        }

        public string GetChatHistoryAsJson()
        {
            return JsonSerializer.Serialize(chatHistory.ToList());
        }

        public async Task OnJoinRoom(RoomInfo roomInfo, UserInfo user, UpdateUserPos updateUserPos)
        {
            using (var scope = scopeFactory.CreateScope())
            {
                var wsHub = scope.ServiceProvider.GetRequiredService<IHubContext<ChatHub>>();
                await wsHub.Clients.All.SendAsync("OnJoinRoom", roomInfo, user, updateUserPos);
            }
        }

        public async Task OnSyncRoom(UserInfo user, List<UpdateUserPos> updateUserPos)
        {
            using (var scope = scopeFactory.CreateScope())
            {
                var wsHub = scope.ServiceProvider.GetRequiredService<IHubContext<ChatHub>>();
                await wsHub.Clients.All.SendAsync("OnSyncRoom", user, updateUserPos);
            }
        }

        public async Task OnLeaveRoom(LeaveRoom leaveRoom)
        {
            using (var scope = scopeFactory.CreateScope())
            {
                var wsHub = scope.ServiceProvider.GetRequiredService<IHubContext<ChatHub>>();
                await wsHub.Clients.All.SendAsync("OnLeaveRoom", leaveRoom);
            }
        }

        public async Task OnUpdateUserPos(UpdateUserPos updatePos)
        {
            using (var scope = scopeFactory.CreateScope())
            {
                var wsHub = scope.ServiceProvider.GetRequiredService<IHubContext<ChatHub>>();
                await wsHub.Clients.All.SendAsync("OnUpdateUserPos", updatePos);
            }
        }

        public async Task OnChatMessage(ChatMessage chatMessage)
        {
            using (var scope = scopeFactory.CreateScope())
            {
                var wsHub = scope.ServiceProvider.GetRequiredService<IHubContext<ChatHub>>();
                await wsHub.Clients.All.SendAsync("OnChatMessage", chatMessage);
            }
        }
    }
}

RoomActor에 사람이 아닌 자동응답을 하는 봇객체를 1개 추가하고싶습니다. JoinRoom 이벤트를 활용해 UserInfo를 추가 참고로 해당 모델은 액터모델이며 액터모델 특성을 고려해 작성

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;

using Akka.Actor;
using Akka.Event;

using BlazorChatApp.Shared;

using Microsoft.AspNetCore.SignalR;
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.DependencyInjection;

namespace BlazorChatApp.Server.Hubs
{
    public class RoomActor : ReceiveActor
    {
        private readonly ILoggingAdapter log = Context.GetLogger();

        public Dictionary<string, UpdateUserPos> users = new Dictionary<string, UpdateUserPos>();

        private string roomName;

        private int userAutoNo = 0;

        private readonly IServiceScopeFactory scopeFactory;

        private readonly ConcurrentQueue<ChatMessage> chatHistory = new ConcurrentQueue<ChatMessage>();

        private const int MaxChatHistoryCount = 50;

        private readonly UserInfo botUserInfo;
        private readonly UpdateUserPos botUpdateUserPos;

        Random random = new Random();

        public RoomActor(string _roomName, IServiceScopeFactory _scopeFactory)
        {
            scopeFactory = _scopeFactory;

            roomName = _roomName;

            log.Info($"Create Room{roomName}");

            // Initialize bot user
            botUserInfo = new UserInfo()
            {
                Id = "bot",
                Name = "ChatBot",
                Color = "#FF5733"
            };

            botUpdateUserPos = new UpdateUserPos()
            {
                Id = botUserInfo.Id,
                Name = botUserInfo.Name,
                PosX = 0,
                PosY = 0,
                AbsPosX = 0,
                AbsPosY = 0,
                ConnectionId = "bot-connection"
            };

            users[botUserInfo.Id] = botUpdateUserPos;

            Receive<RoomCmd>(cmd => {
                log.Info("Received String message: {0}", cmd);
                //Sender.Tell(message);                
            });

            Receive<JoinRoom>(async cmd => {
                userAutoNo++;
                string jsonString = JsonSerializer.Serialize(cmd);
                log.Info("Received JoinRoom message: {0}", jsonString);
                string RandomColor = string.Format("#{0:X6}", random.Next(0xFFFFFF));

                UserInfo userInfo = new UserInfo()
                {
                    Id = cmd.UserInfo.Id,
                    Name = $"User-{userAutoNo}",
                    Color = RandomColor
                };

                // Default Position
                double posx = random.Next(0, 300);
                double posy = random.Next(400, 550);

                UpdateUserPos updateUserPos = new UpdateUserPos()
                {
                    Id = cmd.UserInfo.Id,
                    Name = $"User-{userAutoNo}",
                    PosX = posx,
                    PosY = posy,
                    AbsPosX = posx,
                    AbsPosY = posy,
                    ConnectionId = cmd.ConnectionId
                };

                if (!users.ContainsKey(updateUserPos.Id))
                    users[cmd.UserInfo.Id] = updateUserPos;

                await OnJoinRoom(cmd.RoomInfo, userInfo, updateUserPos);
            });

            Receive<SyncRoom>(async cmd => {
                string jsonString = JsonSerializer.Serialize(cmd);
                log.Info("Received SyncRoom message: {0}", jsonString);

                List<UpdateUserPos> updateUserPosList = users.Values.ToList();
                string RandomColor = string.Format("#{0:X6}", random.Next(0xFFFFFF));

                UserInfo userInfo = new UserInfo()
                {
                    Id = cmd.UserInfo.Id,
                    Name = $"User-{userAutoNo}",
                    Color = RandomColor
                };

                await OnSyncRoom(userInfo, updateUserPosList);
            });

            Receive<ChatMessage>(async cmd => {
                string jsonString = JsonSerializer.Serialize(cmd);
                log.Info("Received ChatMessage message: {0}", jsonString);

                ChatMessage chatMessage = new ChatMessage()
                {
                    From = cmd.From,
                    Message = cmd.Message
                };

                AddChatMessageToHistory(chatMessage);

                await OnChatMessage(chatMessage);

                // Bot response
                if (cmd.Message.Contains("bot", StringComparison.OrdinalIgnoreCase))
                {
                    var botResponse = new ChatMessage()
                    {
                        From = botUserInfo,
                        Message = "Hello! How can I assist you today?"
                    };

                    AddChatMessageToHistory(botResponse);
                    await OnChatMessage(botResponse);
                }
            });

            Receive<UpdateUserPos>(async cmd => {
                string jsonString = JsonSerializer.Serialize(cmd);
                log.Info("Received UpdateUserPos message: {0}", jsonString);

                double AbsPosX = users[cmd.Id].PosX + cmd.PosX;
                double AbsPosY = users[cmd.Id].PosY + cmd.PosY;

                if (users.ContainsKey(cmd.Id))
                {
                    users[cmd.Id].PosX = AbsPosX;
                    users[cmd.Id].PosY = AbsPosY;
                    users[cmd.Id].AbsPosX = AbsPosX;
                    users[cmd.Id].AbsPosY = AbsPosY;
                }

                log.Info($"UpdateUser : X=>{users[cmd.Id].AbsPosX} Y=>{users[cmd.Id].AbsPosY}");

                UpdateUserPos updateUserPos = new UpdateUserPos()
                {
                    Id = cmd.Id,
                    Name = cmd.Name,
                    PosX = cmd.PosX,
                    PosY = cmd.PosY,
                    AbsPosX = AbsPosX,
                    AbsPosY = AbsPosY
                };

                await OnUpdateUserPos(updateUserPos);
            });

            Receive<Disconnect>(async cmd => {
                string jsonString = JsonSerializer.Serialize(cmd);
                log.Info("Received Disconnect message: {0}", jsonString);
                var disconnectUser = users.Values.Where(e => e.ConnectionId == cmd.ConnectionId).FirstOrDefault();

                if (disconnectUser != null)
                {
                    if (users.ContainsKey(disconnectUser.Id))
                    {
                        users.Remove(disconnectUser.Id);

                        var leaveMsg = new LeaveRoom()
                        {
                            UserInfo = new UserInfo() { Id = disconnectUser.Id }
                        };
                        await OnLeaveRoom(leaveMsg);
                    }
                }
            });

            Receive<LeaveRoom>(async cmd => {
                string jsonString = JsonSerializer.Serialize(cmd);
                log.Info("Received LeaveRoom message: {0}", jsonString);

                if (users.ContainsKey(cmd.UserInfo.Id))
                {
                    users.Remove(cmd.UserInfo.Id);
                    await OnLeaveRoom(cmd);
                }
            });
        }

        private void AddChatMessageToHistory(ChatMessage chatMessage)
        {
            chatHistory.Enqueue(chatMessage);
            while (chatHistory.Count > MaxChatHistoryCount)
            {
                chatHistory.TryDequeue(out _);
            }
        }

        private string GetChatHistoryAsJson()
        {
            return JsonSerializer.Serialize(chatHistory.ToList());
        }

        public async Task OnJoinRoom(RoomInfo roomInfo, UserInfo user, UpdateUserPos updateUserPos)
        {
            using (var scope = scopeFactory.CreateScope())
            {
                var wsHub = scope.ServiceProvider.GetRequiredService<IHubContext<ChatHub>>();
                await wsHub.Clients.All.SendAsync("OnJoinRoom", roomInfo, user, updateUserPos);
            }
        }

        public async Task OnSyncRoom(UserInfo user, List<UpdateUserPos> updateUserPos)
        {
            using (var scope = scopeFactory.CreateScope())
            {
                var wsHub = scope.ServiceProvider.GetRequiredService<IHubContext<ChatHub>>();
                await wsHub.Clients.All.SendAsync("OnSyncRoom", user, updateUserPos);
            }
        }

        public async Task OnLeaveRoom(LeaveRoom leaveRoom)
        {
            using (var scope = scopeFactory.CreateScope())
            {
                var wsHub = scope.ServiceProvider.GetRequiredService<IHubContext<ChatHub>>();
                await wsHub.Clients.All.SendAsync("OnLeaveRoom", leaveRoom);
            }
        }

        public async Task OnUpdateUserPos(UpdateUserPos updatePos)
        {
            using (var scope = scopeFactory.CreateScope())
            {
                var wsHub = scope.ServiceProvider.GetRequiredService<IHubContext<ChatHub>>();
                await wsHub.Clients.All.SendAsync("OnUpdateUserPos", updatePos);
            }
        }

        public async Task OnChatMessage(ChatMessage chatMessage)
        {
            using (var scope = scopeFactory.CreateScope())
            {
                var wsHub = scope.ServiceProvider.GetRequiredService<IHubContext<ChatHub>>();
                await wsHub.Clients.All.SendAsync("OnChatMessage", chatMessage);
            }
        }
    }
}

RoomActor 에 타이머기능을 넣어죠 10초에한번씩 반복작동 이벤트를 발생 액터타이머를 이용

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;

using Akka.Actor;
using Akka.Event;

using BlazorChatApp.Shared;

using Microsoft.AspNetCore.SignalR;
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.DependencyInjection;

namespace BlazorChatApp.Server.Hubs
{
    public class RoomActor : ReceiveActor
    {
        private readonly ILoggingAdapter log = Context.GetLogger();

        public Dictionary<string, UpdateUserPos> users = new Dictionary<string, UpdateUserPos>();

        private string roomName;

        private int userAutoNo = 0;

        private readonly IServiceScopeFactory scopeFactory;

        private readonly ConcurrentQueue<ChatMessage> chatHistory = new ConcurrentQueue<ChatMessage>();

        private const int MaxChatHistoryCount = 50;

        private readonly UserInfo botUserInfo;
        private readonly UpdateUserPos botUpdateUserPos;

        Random random = new Random();

        public RoomActor(string _roomName, IServiceScopeFactory _scopeFactory)
        {
            scopeFactory = _scopeFactory;

            roomName = _roomName;

            log.Info($"Create Room{roomName}");

            string RandomColor = string.Format("#{0:X6}", random.Next(0xFFFFFF));

            // Initialize bot user
            botUserInfo = new UserInfo()
            {
                Id = "bot-1",
                Name = "ChatBot-1",
                Color = RandomColor
            };

            botUpdateUserPos = new UpdateUserPos()
            {
                Id = botUserInfo.Id,
                Name = botUserInfo.Name,
                PosX = 250,
                PosY = 250,
                AbsPosX = 250,
                AbsPosY = 250,
                ConnectionId = "bot-connection"
            };

            users[botUserInfo.Id] = botUpdateUserPos;

            // Schedule a timer to send a Tick message every 10 seconds
            Context.System.Scheduler.ScheduleTellRepeatedly(
                TimeSpan.FromSeconds(10),
                TimeSpan.FromSeconds(10),
                Self,
                new Tick(),
                Self
            );

            Receive<RoomCmd>(cmd => {
                log.Info("Received String message: {0}", cmd);
                //Sender.Tell(message);                
            });

            Receive<JoinRoom>(async cmd => {
                userAutoNo++;
                string jsonString = JsonSerializer.Serialize(cmd);
                log.Info("Received JoinRoom message: {0}", jsonString);
                string RandomColor = string.Format("#{0:X6}", random.Next(0xFFFFFF));

                UserInfo userInfo = new UserInfo()
                {
                    Id = cmd.UserInfo.Id,
                    Name = $"User-{userAutoNo}",
                    Color = RandomColor
                };

                // Default Position
                double posx = random.Next(0, 300);
                double posy = random.Next(400, 550);

                UpdateUserPos updateUserPos = new UpdateUserPos()
                {
                    Id = cmd.UserInfo.Id,
                    Name = $"User-{userAutoNo}",
                    PosX = posx,
                    PosY = posy,
                    AbsPosX = posx,
                    AbsPosY = posy,
                    ConnectionId = cmd.ConnectionId
                };

                if (!users.ContainsKey(updateUserPos.Id))
                    users[cmd.UserInfo.Id] = updateUserPos;

                await OnJoinRoom(cmd.RoomInfo, userInfo, updateUserPos);
            });

            Receive<SyncRoom>(async cmd => {
                string jsonString = JsonSerializer.Serialize(cmd);
                log.Info("Received SyncRoom message: {0}", jsonString);

                List<UpdateUserPos> updateUserPosList = users.Values.ToList();
                string RandomColor = string.Format("#{0:X6}", random.Next(0xFFFFFF));

                UserInfo userInfo = new UserInfo()
                {
                    Id = cmd.UserInfo.Id,
                    Name = $"User-{userAutoNo}",
                    Color = RandomColor
                };

                await OnSyncRoom(botUserInfo, updateUserPosList);

                await OnSyncRoom(userInfo, updateUserPosList);
            });

            Receive<ChatMessage>(async cmd => {
                string jsonString = JsonSerializer.Serialize(cmd);
                log.Info("Received ChatMessage message: {0}", jsonString);

                ChatMessage chatMessage = new ChatMessage()
                {
                    From = cmd.From,
                    Message = cmd.Message
                };

                AddChatMessageToHistory(chatMessage);

                await OnChatMessage(chatMessage);
            });

            Receive<UpdateUserPos>(async cmd => {
                string jsonString = JsonSerializer.Serialize(cmd);
                log.Info("Received UpdateUserPos message: {0}", jsonString);

                double AbsPosX = users[cmd.Id].PosX + cmd.PosX;
                double AbsPosY = users[cmd.Id].PosY + cmd.PosY;

                if (users.ContainsKey(cmd.Id))
                {
                    users[cmd.Id].PosX = AbsPosX;
                    users[cmd.Id].PosY = AbsPosY;
                    users[cmd.Id].AbsPosX = AbsPosX;
                    users[cmd.Id].AbsPosY = AbsPosY;
                }

                log.Info($"UpdateUser : X=>{users[cmd.Id].AbsPosX} Y=>{users[cmd.Id].AbsPosY}");

                UpdateUserPos updateUserPos = new UpdateUserPos()
                {
                    Id = cmd.Id,
                    Name = cmd.Name,
                    PosX = cmd.PosX,
                    PosY = cmd.PosY,
                    AbsPosX = AbsPosX,
                    AbsPosY = AbsPosY
                };

                await OnUpdateUserPos(updateUserPos);
            });

            Receive<Disconnect>(async cmd => {
                string jsonString = JsonSerializer.Serialize(cmd);
                log.Info("Received Disconnect message: {0}", jsonString);
                var disconnectUser = users.Values.Where(e => e.ConnectionId == cmd.ConnectionId).FirstOrDefault();

                if (disconnectUser != null)
                {
                    if (users.ContainsKey(disconnectUser.Id))
                    {
                        users.Remove(disconnectUser.Id);

                        var leaveMsg = new LeaveRoom()
                        {
                            UserInfo = new UserInfo() { Id = disconnectUser.Id }
                        };
                        await OnLeaveRoom(leaveMsg);
                    }
                }
            });

            Receive<LeaveRoom>(async cmd => {
                string jsonString = JsonSerializer.Serialize(cmd);
                log.Info("Received LeaveRoom message: {0}", jsonString);

                if (users.ContainsKey(cmd.UserInfo.Id))
                {
                    users.Remove(cmd.UserInfo.Id);
                    await OnLeaveRoom(cmd);
                }
            });

            // Handle Tick message
            Receive<Tick>(_ => {
                log.Info("Tick event triggered");
                // Add your custom logic here to be executed every 10 seconds
            });
        }

        private void AddChatMessageToHistory(ChatMessage chatMessage)
        {
            chatHistory.Enqueue(chatMessage);
            while (chatHistory.Count > MaxChatHistoryCount)
            {
                chatHistory.TryDequeue(out _);
            }
        }

        private string GetChatHistoryAsJson()
        {
            return JsonSerializer.Serialize(chatHistory.ToList());
        }

        public async Task OnJoinRoom(RoomInfo roomInfo, UserInfo user, UpdateUserPos updateUserPos)
        {
            using (var scope = scopeFactory.CreateScope())
            {
                var wsHub = scope.ServiceProvider.GetRequiredService<IHubContext<ChatHub>>();
                await wsHub.Clients.All.SendAsync("OnJoinRoom", roomInfo, user, updateUserPos);
            }
        }

        public async Task OnSyncRoom(UserInfo user, List<UpdateUserPos> updateUserPos)
        {
            using (var scope = scopeFactory.CreateScope())
            {
                var wsHub = scope.ServiceProvider.GetRequiredService<IHubContext<ChatHub>>();
                await wsHub.Clients.All.SendAsync("OnSyncRoom", user, updateUserPos);
            }
        }

        public async Task OnLeaveRoom(LeaveRoom leaveRoom)
        {
            using (var scope = scopeFactory.CreateScope())
            {
                var wsHub = scope.ServiceProvider.GetRequiredService<IHubContext<ChatHub>>();
                await wsHub.Clients.All.SendAsync("OnLeaveRoom", leaveRoom);
            }
        }

        public async Task OnUpdateUserPos(UpdateUserPos updatePos)
        {
            using (var scope = scopeFactory.CreateScope())
            {
                var wsHub = scope.ServiceProvider.GetRequiredService<IHubContext<ChatHub>>();
                await wsHub.Clients.All.SendAsync("OnUpdateUserPos", updatePos);
            }
        }

        public async Task OnChatMessage(ChatMessage chatMessage)
        {
            using (var scope = scopeFactory.CreateScope())
            {
                var wsHub = scope.ServiceProvider.GetRequiredService<IHubContext<ChatHub>>();
                await wsHub.Clients.All.SendAsync("OnChatMessage", chatMessage);
            }
        }

        // Define Tick message class
        public class Tick { }
    }
}

OnUpdateUserPos에 botUpdateUserPos 를 이용해 Tick에서 특정반경내에서 알아서 움직이게 코드추가

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;

using Akka.Actor;
using Akka.Event;

using BlazorChatApp.Shared;

using Microsoft.AspNetCore.SignalR;
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.DependencyInjection;

namespace BlazorChatApp.Server.Hubs
{
    // Define Tick message class
    public class Tick { }

    public class RoomActor : ReceiveActor
    {
        private readonly ILoggingAdapter log = Context.GetLogger();

        public Dictionary<string, UpdateUserPos> users = new Dictionary<string, UpdateUserPos>();

        private string roomName;

        private int userAutoNo = 0;

        private readonly IServiceScopeFactory scopeFactory;

        private readonly ConcurrentQueue<ChatMessage> chatHistory = new ConcurrentQueue<ChatMessage>();

        private const int MaxChatHistoryCount = 50;

        private readonly UserInfo botUserInfo;
        private readonly UpdateUserPos botUpdateUserPos;

        Random random = new Random();

        public RoomActor(string _roomName, IServiceScopeFactory _scopeFactory)
        {
            scopeFactory = _scopeFactory;

            roomName = _roomName;

            log.Info($"Create Room{roomName}");

            string RandomColor = string.Format("#{0:X6}", random.Next(0xFFFFFF));

            // Schedule a timer to send a Tick message every 10 seconds
            Context.System.Scheduler.ScheduleTellRepeatedly(
                TimeSpan.FromSeconds(10),
                TimeSpan.FromSeconds(10),
                Self,
                new Tick(),
                Self
            );

            // Initialize bot user
            botUserInfo = new UserInfo()
            {
                Id = "bot-1",
                Name = "ChatBot-1",
                Color = RandomColor
            };

            botUpdateUserPos = new UpdateUserPos()
            {
                Id = botUserInfo.Id,
                Name = botUserInfo.Name,
                PosX = 250,
                PosY = 250,
                AbsPosX = 250,
                AbsPosY = 250,
                ConnectionId = "bot-connection"
            };

            users[botUserInfo.Id] = botUpdateUserPos;

            Receive<RoomCmd>(cmd => {
                log.Info("Received String message: {0}", cmd);
                //Sender.Tell(message);                
            });

            Receive<JoinRoom>(async cmd => {
                userAutoNo++;
                string jsonString = JsonSerializer.Serialize(cmd);
                log.Info("Received JoinRoom message: {0}", jsonString);
                string RandomColor = string.Format("#{0:X6}", random.Next(0xFFFFFF));

                UserInfo userInfo = new UserInfo()
                {
                    Id = cmd.UserInfo.Id,
                    Name = $"User-{userAutoNo}",
                    Color = RandomColor
                };

                // Default Position
                double posx = random.Next(0, 300);
                double posy = random.Next(400, 550);

                UpdateUserPos updateUserPos = new UpdateUserPos()
                {
                    Id = cmd.UserInfo.Id,
                    Name = $"User-{userAutoNo}",
                    PosX = posx,
                    PosY = posy,
                    AbsPosX = posx,
                    AbsPosY = posy,
                    ConnectionId = cmd.ConnectionId
                };

                if (!users.ContainsKey(updateUserPos.Id))
                    users[cmd.UserInfo.Id] = updateUserPos;

                await OnJoinRoom(cmd.RoomInfo, userInfo, updateUserPos);
            });

            Receive<SyncRoom>(async cmd => {
                string jsonString = JsonSerializer.Serialize(cmd);
                log.Info("Received SyncRoom message: {0}", jsonString);

                List<UpdateUserPos> updateUserPosList = users.Values.ToList();
                string RandomColor = string.Format("#{0:X6}", random.Next(0xFFFFFF));

                UserInfo userInfo = new UserInfo()
                {
                    Id = cmd.UserInfo.Id,
                    Name = $"User-{userAutoNo}",
                    Color = RandomColor
                };

                await OnSyncRoom(botUserInfo, updateUserPosList);

                await OnSyncRoom(userInfo, updateUserPosList);
            });

            Receive<ChatMessage>(async cmd => {
                string jsonString = JsonSerializer.Serialize(cmd);
                log.Info("Received ChatMessage message: {0}", jsonString);

                ChatMessage chatMessage = new ChatMessage()
                {
                    From = cmd.From,
                    Message = cmd.Message
                };

                AddChatMessageToHistory(chatMessage);

                await OnChatMessage(chatMessage);
            });

            Receive<UpdateUserPos>(async cmd => {
                string jsonString = JsonSerializer.Serialize(cmd);
                log.Info("Received UpdateUserPos message: {0}", jsonString);

                double AbsPosX = users[cmd.Id].PosX + cmd.PosX;
                double AbsPosY = users[cmd.Id].PosY + cmd.PosY;

                if (users.ContainsKey(cmd.Id))
                {
                    users[cmd.Id].PosX = AbsPosX;
                    users[cmd.Id].PosY = AbsPosY;
                    users[cmd.Id].AbsPosX = AbsPosX;
                    users[cmd.Id].AbsPosY = AbsPosY;
                }

                log.Info($"UpdateUser : X=>{users[cmd.Id].AbsPosX} Y=>{users[cmd.Id].AbsPosY}");

                UpdateUserPos updateUserPos = new UpdateUserPos()
                {
                    Id = cmd.Id,
                    Name = cmd.Name,
                    PosX = cmd.PosX,
                    PosY = cmd.PosY,
                    AbsPosX = AbsPosX,
                    AbsPosY = AbsPosY
                };

                await OnUpdateUserPos(updateUserPos);
            });

            Receive<Disconnect>(async cmd => {
                string jsonString = JsonSerializer.Serialize(cmd);
                log.Info("Received Disconnect message: {0}", jsonString);
                var disconnectUser = users.Values.Where(e => e.ConnectionId == cmd.ConnectionId).FirstOrDefault();

                if (disconnectUser != null)
                {
                    if (users.ContainsKey(disconnectUser.Id))
                    {
                        users.Remove(disconnectUser.Id);

                        var leaveMsg = new LeaveRoom()
                        {
                            UserInfo = new UserInfo() { Id = disconnectUser.Id }
                        };
                        await OnLeaveRoom(leaveMsg);
                    }
                }
            });

            Receive<LeaveRoom>(async cmd => {
                string jsonString = JsonSerializer.Serialize(cmd);
                log.Info("Received LeaveRoom message: {0}", jsonString);

                if (users.ContainsKey(cmd.UserInfo.Id))
                {
                    users.Remove(cmd.UserInfo.Id);
                    await OnLeaveRoom(cmd);
                }
            });

            // Handle Tick message
            Receive<Tick>(_ => {
                log.Info("Tick event triggered");

                // Move botUpdateUserPos within a specific range
                double moveX = random.NextDouble() * 20 - 10; // Move between -10 and 10
                double moveY = random.NextDouble() * 20 - 10; // Move between -10 and 10

                botUpdateUserPos.PosX += moveX;
                botUpdateUserPos.PosY += moveY;
                botUpdateUserPos.AbsPosX += moveX;
                botUpdateUserPos.AbsPosY += moveY;

                // Ensure bot stays within a specific range (e.g., 200 to 300 for both X and Y)
                botUpdateUserPos.PosX = Math.Clamp(botUpdateUserPos.PosX, 200, 300);
                botUpdateUserPos.PosY = Math.Clamp(botUpdateUserPos.PosY, 200, 300);
                botUpdateUserPos.AbsPosX = Math.Clamp(botUpdateUserPos.AbsPosX, 200, 300);
                botUpdateUserPos.AbsPosY = Math.Clamp(botUpdateUserPos.AbsPosY, 200, 300);

                // Update bot position
                Self.Tell(botUpdateUserPos);
            });
        }

        private void AddChatMessageToHistory(ChatMessage chatMessage)
        {
            chatHistory.Enqueue(chatMessage);
            while (chatHistory.Count > MaxChatHistoryCount)
            {
                chatHistory.TryDequeue(out _);
            }
        }

        private string GetChatHistoryAsJson()
        {
            return JsonSerializer.Serialize(chatHistory.ToList());
        }

        public async Task OnJoinRoom(RoomInfo roomInfo, UserInfo user, UpdateUserPos updateUserPos)
        {
            using (var scope = scopeFactory.CreateScope())
            {
                var wsHub = scope.ServiceProvider.GetRequiredService<IHubContext<ChatHub>>();
                await wsHub.Clients.All.SendAsync("OnJoinRoom", roomInfo, user, updateUserPos);
            }
        }

        public async Task OnSyncRoom(UserInfo user, List<UpdateUserPos> updateUserPos)
        {
            using (var scope = scopeFactory.CreateScope())
            {
                var wsHub = scope.ServiceProvider.GetRequiredService<IHubContext<ChatHub>>();
                await wsHub.Clients.All.SendAsync("OnSyncRoom", user, updateUserPos);
            }
        }

        public async Task OnLeaveRoom(LeaveRoom leaveRoom)
        {
            using (var scope = scopeFactory.CreateScope())
            {
                var wsHub = scope.ServiceProvider.GetRequiredService<IHubContext<ChatHub>>();
                await wsHub.Clients.All.SendAsync("OnLeaveRoom", leaveRoom);
            }
        }

        public async Task OnUpdateUserPos(UpdateUserPos updatePos)
        {
            using (var scope = scopeFactory.CreateScope())
            {
                var wsHub = scope.ServiceProvider.GetRequiredService<IHubContext<ChatHub>>();
                await wsHub.Clients.All.SendAsync("OnUpdateUserPos", updatePos);
            }
        }

        public async Task OnChatMessage(ChatMessage chatMessage)
        {
            using (var scope = scopeFactory.CreateScope())
            {
                var wsHub = scope.ServiceProvider.GetRequiredService<IHubContext<ChatHub>>();
                await wsHub.Clients.All.SendAsync("OnChatMessage", chatMessage);
            }
        }
    }
}

화면밖에 안나가게 ~ 반대로 움직일수있게 조절 한번에 움직이는 것은 5로 제약

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;

using Akka.Actor;
using Akka.Event;

using BlazorChatApp.Shared;

using Microsoft.AspNetCore.SignalR;
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.DependencyInjection;

namespace BlazorChatApp.Server.Hubs
{
    // Define Tick message class
    public class Tick { }

    public class RoomActor : ReceiveActor
    {
        private readonly ILoggingAdapter log = Context.GetLogger();

        public Dictionary<string, UpdateUserPos> users = new Dictionary<string, UpdateUserPos>();

        private string roomName;

        private int userAutoNo = 0;

        private readonly IServiceScopeFactory scopeFactory;

        private readonly ConcurrentQueue<ChatMessage> chatHistory = new ConcurrentQueue<ChatMessage>();

        private const int MaxChatHistoryCount = 50;

        private readonly UserInfo botUserInfo;
        private readonly UpdateUserPos botUpdateUserPos;

        Random random = new Random();

        public RoomActor(string _roomName, IServiceScopeFactory _scopeFactory)
        {
            scopeFactory = _scopeFactory;

            roomName = _roomName;

            log.Info($"Create Room{roomName}");

            string RandomColor = string.Format("#{0:X6}", random.Next(0xFFFFFF));

            // Schedule a timer to send a Tick message every 10 seconds
            Context.System.Scheduler.ScheduleTellRepeatedly(
                TimeSpan.FromSeconds(10),
                TimeSpan.FromSeconds(3),
                Self,
                new Tick(),
                Self
            );

            // Initialize bot user
            botUserInfo = new UserInfo()
            {
                Id = "bot-1",
                Name = "ChatBot-1",
                Color = RandomColor
            };

            botUpdateUserPos = new UpdateUserPos()
            {
                Id = botUserInfo.Id,
                Name = botUserInfo.Name,
                PosX = 250,
                PosY = 250,
                AbsPosX = 250,
                AbsPosY = 250,
                ConnectionId = "bot-connection"
            };

            users[botUserInfo.Id] = botUpdateUserPos;

            Receive<RoomCmd>(cmd => {
                log.Info("Received String message: {0}", cmd);
                //Sender.Tell(message);                
            });

            Receive<JoinRoom>(async cmd => {
                userAutoNo++;
                string jsonString = JsonSerializer.Serialize(cmd);
                log.Info("Received JoinRoom message: {0}", jsonString);
                string RandomColor = string.Format("#{0:X6}", random.Next(0xFFFFFF));

                UserInfo userInfo = new UserInfo()
                {
                    Id = cmd.UserInfo.Id,
                    Name = $"User-{userAutoNo}",
                    Color = RandomColor
                };

                // Default Position
                double posx = random.Next(0, 300);
                double posy = random.Next(400, 550);

                UpdateUserPos updateUserPos = new UpdateUserPos()
                {
                    Id = cmd.UserInfo.Id,
                    Name = $"User-{userAutoNo}",
                    PosX = posx,
                    PosY = posy,
                    AbsPosX = posx,
                    AbsPosY = posy,
                    ConnectionId = cmd.ConnectionId
                };

                if (!users.ContainsKey(updateUserPos.Id))
                    users[cmd.UserInfo.Id] = updateUserPos;

                await OnJoinRoom(cmd.RoomInfo, userInfo, updateUserPos);
            });

            Receive<SyncRoom>(async cmd => {
                string jsonString = JsonSerializer.Serialize(cmd);
                log.Info("Received SyncRoom message: {0}", jsonString);

                List<UpdateUserPos> updateUserPosList = users.Values.ToList();
                string RandomColor = string.Format("#{0:X6}", random.Next(0xFFFFFF));

                UserInfo userInfo = new UserInfo()
                {
                    Id = cmd.UserInfo.Id,
                    Name = $"User-{userAutoNo}",
                    Color = RandomColor
                };

                await OnSyncRoom(botUserInfo, updateUserPosList);

                await OnSyncRoom(userInfo, updateUserPosList);
            });

            Receive<ChatMessage>(async cmd => {
                string jsonString = JsonSerializer.Serialize(cmd);
                log.Info("Received ChatMessage message: {0}", jsonString);

                ChatMessage chatMessage = new ChatMessage()
                {
                    From = cmd.From,
                    Message = cmd.Message
                };

                AddChatMessageToHistory(chatMessage);

                await OnChatMessage(chatMessage);
            });

            Receive<UpdateUserPos>(async cmd => {
                string jsonString = JsonSerializer.Serialize(cmd);
                log.Info("Received UpdateUserPos message: {0}", jsonString);

                double AbsPosX = users[cmd.Id].PosX + cmd.PosX;
                double AbsPosY = users[cmd.Id].PosY + cmd.PosY;

                if (users.ContainsKey(cmd.Id))
                {
                    users[cmd.Id].PosX = AbsPosX;
                    users[cmd.Id].PosY = AbsPosY;
                    users[cmd.Id].AbsPosX = AbsPosX;
                    users[cmd.Id].AbsPosY = AbsPosY;
                }

                log.Info($"UpdateUser : X=>{users[cmd.Id].AbsPosX} Y=>{users[cmd.Id].AbsPosY}");

                UpdateUserPos updateUserPos = new UpdateUserPos()
                {
                    Id = cmd.Id,
                    Name = cmd.Name,
                    PosX = cmd.PosX,
                    PosY = cmd.PosY,
                    AbsPosX = AbsPosX,
                    AbsPosY = AbsPosY
                };

                await OnUpdateUserPos(updateUserPos);
            });

            Receive<Disconnect>(async cmd => {
                string jsonString = JsonSerializer.Serialize(cmd);
                log.Info("Received Disconnect message: {0}", jsonString);
                var disconnectUser = users.Values.Where(e => e.ConnectionId == cmd.ConnectionId).FirstOrDefault();

                if (disconnectUser != null)
                {
                    if (users.ContainsKey(disconnectUser.Id))
                    {
                        users.Remove(disconnectUser.Id);

                        var leaveMsg = new LeaveRoom()
                        {
                            UserInfo = new UserInfo() { Id = disconnectUser.Id }
                        };
                        await OnLeaveRoom(leaveMsg);
                    }
                }
            });

            Receive<LeaveRoom>(async cmd => {
                string jsonString = JsonSerializer.Serialize(cmd);
                log.Info("Received LeaveRoom message: {0}", jsonString);

                if (users.ContainsKey(cmd.UserInfo.Id))
                {
                    users.Remove(cmd.UserInfo.Id);
                    await OnLeaveRoom(cmd);
                }
            });

            // Handle Tick message
            Receive<Tick>(_ => {
                log.Info("Tick event triggered");

                // Move botUpdateUserPos within a specific range
                double moveX = random.NextDouble() * 10 - 5; // Move between -5 and 5
                double moveY = random.NextDouble() * 10 - 5; // Move between -5 and 5

                botUpdateUserPos.PosX += moveX;
                botUpdateUserPos.PosY += moveY;
                botUpdateUserPos.AbsPosX += moveX;
                botUpdateUserPos.AbsPosY += moveY;

                // Ensure bot stays within a specific range (e.g., 200 to 300 for both X and Y)
                if (botUpdateUserPos.PosX < 200 || botUpdateUserPos.PosX > 300)
                {
                    botUpdateUserPos.PosX -= moveX; // Reverse the movement
                    botUpdateUserPos.AbsPosX -= moveX;
                }

                if (botUpdateUserPos.PosY < 200 || botUpdateUserPos.PosY > 300)
                {
                    botUpdateUserPos.PosY -= moveY; // Reverse the movement
                    botUpdateUserPos.AbsPosY -= moveY;
                }

                // Update bot position
                Self.Tell(botUpdateUserPos);
            });
        }

        private void AddChatMessageToHistory(ChatMessage chatMessage)
        {
            chatHistory.Enqueue(chatMessage);
            while (chatHistory.Count > MaxChatHistoryCount)
            {
                chatHistory.TryDequeue(out _);
            }
        }

        private string GetChatHistoryAsJson()
        {
            return JsonSerializer.Serialize(chatHistory.ToList());
        }

        public async Task OnJoinRoom(RoomInfo roomInfo, UserInfo user, UpdateUserPos updateUserPos)
        {
            using (var scope = scopeFactory.CreateScope())
            {
                var wsHub = scope.ServiceProvider.GetRequiredService<IHubContext<ChatHub>>();
                await wsHub.Clients.All.SendAsync("OnJoinRoom", roomInfo, user, updateUserPos);
            }
        }

        public async Task OnSyncRoom(UserInfo user, List<UpdateUserPos> updateUserPos)
        {
            using (var scope = scopeFactory.CreateScope())
            {
                var wsHub = scope.ServiceProvider.GetRequiredService<IHubContext<ChatHub>>();
                await wsHub.Clients.All.SendAsync("OnSyncRoom", user, updateUserPos);
            }
        }

        public async Task OnLeaveRoom(LeaveRoom leaveRoom)
        {
            using (var scope = scopeFactory.CreateScope())
            {
                var wsHub = scope.ServiceProvider.GetRequiredService<IHubContext<ChatHub>>();
                await wsHub.Clients.All.SendAsync("OnLeaveRoom", leaveRoom);
            }
        }

        public async Task OnUpdateUserPos(UpdateUserPos updatePos)
        {
            using (var scope = scopeFactory.CreateScope())
            {
                var wsHub = scope.ServiceProvider.GetRequiredService<IHubContext<ChatHub>>();
                await wsHub.Clients.All.SendAsync("OnUpdateUserPos", updatePos);
            }
        }

        public async Task OnChatMessage(ChatMessage chatMessage)
        {
            using (var scope = scopeFactory.CreateScope())
            {
                var wsHub = scope.ServiceProvider.GetRequiredService<IHubContext<ChatHub>>();
                await wsHub.Clients.All.SendAsync("OnChatMessage", chatMessage);
            }
        }
    }
}

소원을 들어주는 메타공간 새해복 많이 받으세요 메시지를 포함 다양한 새해 인사를 랜덤하게 덕담으로

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;

using Akka.Actor;
using Akka.Event;

using BlazorChatApp.Shared;

using Microsoft.AspNetCore.SignalR;
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.DependencyInjection;

namespace BlazorChatApp.Server.Hubs
{
    // Define Tick message class
    public class Tick { }

    public class RoomActor : ReceiveActor
    {
        private readonly ILoggingAdapter log = Context.GetLogger();

        public Dictionary<string, UpdateUserPos> users = new Dictionary<string, UpdateUserPos>();

        private string roomName;

        private int userAutoNo = 0;

        private readonly IServiceScopeFactory scopeFactory;

        private readonly ConcurrentQueue<ChatMessage> chatHistory = new ConcurrentQueue<ChatMessage>();

        private const int MaxChatHistoryCount = 50;

        private readonly UserInfo botUserInfo;
        private readonly UpdateUserPos botUpdateUserPos;

        Random random = new Random();

        public RoomActor(string _roomName, IServiceScopeFactory _scopeFactory)
        {
            scopeFactory = _scopeFactory;

            roomName = _roomName;

            log.Info($"Create Room{roomName}");

            string RandomColor = string.Format("#{0:X6}", random.Next(0xFFFFFF));

            // Schedule a timer to send a Tick message every 10 seconds
            Context.System.Scheduler.ScheduleTellRepeatedly(
                TimeSpan.FromSeconds(10),
                TimeSpan.FromSeconds(3),
                Self,
                new Tick(),
                Self
            );

            // Initialize bot user
            botUserInfo = new UserInfo()
            {
                Id = "bot-1",
                Name = "ChatBot-1",
                Color = RandomColor
            };

            botUpdateUserPos = new UpdateUserPos()
            {
                Id = botUserInfo.Id,
                Name = botUserInfo.Name,
                PosX = 250,
                PosY = 250,
                AbsPosX = 250,
                AbsPosY = 250,
                ConnectionId = "bot-connection"
            };

            users[botUserInfo.Id] = botUpdateUserPos;

            Receive<RoomCmd>(cmd => {
                log.Info("Received String message: {0}", cmd);
                //Sender.Tell(message);                
            });

            Receive<JoinRoom>(async cmd => {
                userAutoNo++;
                string jsonString = JsonSerializer.Serialize(cmd);
                log.Info("Received JoinRoom message: {0}", jsonString);
                string RandomColor = string.Format("#{0:X6}", random.Next(0xFFFFFF));

                UserInfo userInfo = new UserInfo()
                {
                    Id = cmd.UserInfo.Id,
                    Name = $"User-{userAutoNo}",
                    Color = RandomColor
                };

                // Default Position
                double posx = random.Next(0, 300);
                double posy = random.Next(400, 550);

                UpdateUserPos updateUserPos = new UpdateUserPos()
                {
                    Id = cmd.UserInfo.Id,
                    Name = $"User-{userAutoNo}",
                    PosX = posx,
                    PosY = posy,
                    AbsPosX = posx,
                    AbsPosY = posy,
                    ConnectionId = cmd.ConnectionId
                };

                if (!users.ContainsKey(updateUserPos.Id))
                    users[cmd.UserInfo.Id] = updateUserPos;

                await OnJoinRoom(cmd.RoomInfo, userInfo, updateUserPos);
            });

            Receive<SyncRoom>(async cmd => {
                string jsonString = JsonSerializer.Serialize(cmd);
                log.Info("Received SyncRoom message: {0}", jsonString);

                List<UpdateUserPos> updateUserPosList = users.Values.ToList();
                string RandomColor = string.Format("#{0:X6}", random.Next(0xFFFFFF));

                UserInfo userInfo = new UserInfo()
                {
                    Id = cmd.UserInfo.Id,
                    Name = $"User-{userAutoNo}",
                    Color = RandomColor
                };

                await OnSyncRoom(botUserInfo, updateUserPosList);

                await OnSyncRoom(userInfo, updateUserPosList);
            });

            Receive<ChatMessage>(async cmd => {
                string jsonString = JsonSerializer.Serialize(cmd);
                log.Info("Received ChatMessage message: {0}", jsonString);

                ChatMessage chatMessage = new ChatMessage()
                {
                    From = cmd.From,
                    Message = cmd.Message
                };

                AddChatMessageToHistory(chatMessage);

                await OnChatMessage(chatMessage);
            });

            Receive<UpdateUserPos>(async cmd => {
                string jsonString = JsonSerializer.Serialize(cmd);
                log.Info("Received UpdateUserPos message: {0}", jsonString);

                double AbsPosX = users[cmd.Id].PosX + cmd.PosX;
                double AbsPosY = users[cmd.Id].PosY + cmd.PosY;

                if (users.ContainsKey(cmd.Id))
                {
                    users[cmd.Id].PosX = AbsPosX;
                    users[cmd.Id].PosY = AbsPosY;
                    users[cmd.Id].AbsPosX = AbsPosX;
                    users[cmd.Id].AbsPosY = AbsPosY;
                }

                log.Info($"UpdateUser : X=>{users[cmd.Id].AbsPosX} Y=>{users[cmd.Id].AbsPosY}");

                UpdateUserPos updateUserPos = new UpdateUserPos()
                {
                    Id = cmd.Id,
                    Name = cmd.Name,
                    PosX = cmd.PosX,
                    PosY = cmd.PosY,
                    AbsPosX = AbsPosX,
                    AbsPosY = AbsPosY
                };

                await OnUpdateUserPos(updateUserPos);
            });

            Receive<Disconnect>(async cmd => {
                string jsonString = JsonSerializer.Serialize(cmd);
                log.Info("Received Disconnect message: {0}", jsonString);
                var disconnectUser = users.Values.Where(e => e.ConnectionId == cmd.ConnectionId).FirstOrDefault();

                if (disconnectUser != null)
                {
                    if (users.ContainsKey(disconnectUser.Id))
                    {
                        users.Remove(disconnectUser.Id);

                        var leaveMsg = new LeaveRoom()
                        {
                            UserInfo = new UserInfo() { Id = disconnectUser.Id }
                        };
                        await OnLeaveRoom(leaveMsg);
                    }
                }
            });

            Receive<LeaveRoom>(async cmd => {
                string jsonString = JsonSerializer.Serialize(cmd);
                log.Info("Received LeaveRoom message: {0}", jsonString);

                if (users.ContainsKey(cmd.UserInfo.Id))
                {
                    users.Remove(cmd.UserInfo.Id);
                    await OnLeaveRoom(cmd);
                }
            });

            // Handle Tick message
            Receive<Tick>(async _ => {
                log.Info("Tick event triggered");

                // Move botUpdateUserPos within a specific range
                double moveX = random.NextDouble() * 40 - 20; // Move between -20 and 20
                double moveY = 0;

                // 25% chance to move in Y direction
                if (random.Next(4) == 0)
                {
                    moveY = random.NextDouble() * 40 - 20; // Move between -20 and 20
                }

                double newPosX = botUpdateUserPos.PosX + moveX;
                double newPosY = botUpdateUserPos.PosY + moveY;

                // Ensure bot stays within the screen boundaries (e.g., 0 to 800 for X and 0 to 600 for Y)
                if (newPosX >= 0 && newPosX <= 800 && newPosY >= 0 && newPosY <= 600)
                {
                    botUpdateUserPos.PosX = newPosX;
                    botUpdateUserPos.PosY = newPosY;
                    botUpdateUserPos.AbsPosX += moveX;
                    botUpdateUserPos.AbsPosY += moveY;

                    // Update bot position
                    Self.Tell(botUpdateUserPos);
                }

                // Send random New Year greeting
                string[] greetings = new string[]
                {
                    "새해 복 많이 받으세요!",
                    "행복한 새해 되세요!",
                    "건강하고 행복한 한 해 되세요!",
                    "새해에는 모든 소원이 이루어지길 바랍니다!",
                    "희망찬 새해 되세요!"
                };

                string randomGreeting = greetings[random.Next(greetings.Length)];

                ChatMessage greetingMessage = new ChatMessage()
                {
                    From = botUserInfo.Name,
                    Message = randomGreeting
                };

                AddChatMessageToHistory(greetingMessage);

                await OnChatMessage(greetingMessage);
            });
        }

        private void AddChatMessageToHistory(ChatMessage chatMessage)
        {
            chatHistory.Enqueue(chatMessage);
            while (chatHistory.Count > MaxChatHistoryCount)
            {
                chatHistory.TryDequeue(out _);
            }
        }

        private string GetChatHistoryAsJson()
        {
            return JsonSerializer.Serialize(chatHistory.ToList());
        }

        public async Task OnJoinRoom(RoomInfo roomInfo, UserInfo user, UpdateUserPos updateUserPos)
        {
            using (var scope = scopeFactory.CreateScope())
            {
                var wsHub = scope.ServiceProvider.GetRequiredService<IHubContext<ChatHub>>();
                await wsHub.Clients.All.SendAsync("OnJoinRoom", roomInfo, user, updateUserPos);
            }
        }

        public async Task OnSyncRoom(UserInfo user, List<UpdateUserPos> updateUserPos)
        {
            using (var scope = scopeFactory.CreateScope())
            {
                var wsHub = scope.ServiceProvider.GetRequiredService<IHubContext<ChatHub>>();
                await wsHub.Clients.All.SendAsync("OnSyncRoom", user, updateUserPos);
            }
        }

        public async Task OnLeaveRoom(LeaveRoom leaveRoom)
        {
            using (var scope = scopeFactory.CreateScope())
            {
                var wsHub = scope.ServiceProvider.GetRequiredService<IHubContext<ChatHub>>();
                await wsHub.Clients.All.SendAsync("OnLeaveRoom", leaveRoom);
            }
        }

        public async Task OnUpdateUserPos(UpdateUserPos updatePos)
        {
            using (var scope = scopeFactory.CreateScope())
            {
                var wsHub = scope.ServiceProvider.GetRequiredService<IHubContext<ChatHub>>();
                await wsHub.Clients.All.SendAsync("OnUpdateUserPos", updatePos);
            }
        }

        public async Task OnChatMessage(ChatMessage chatMessage)
        {
            using (var scope = scopeFactory.CreateScope())
            {
                var wsHub = scope.ServiceProvider.GetRequiredService<IHubContext<ChatHub>>();
                await wsHub.Clients.All.SendAsync("OnChatMessage", chatMessage);
            }
        }
    }
}


명언중에서 새해 힘찬 목표를 가질수 있는 단어로 다시구성

string[] quotes = new string[]
{
    "새해에는 목표를 향해 힘차게 나아가세요!",
    "새해에는 더 큰 도전을 향해 나아가세요!",
    "새해에는 꿈을 이루기 위해 최선을 다하세요!",
    "새해에는 새로운 목표를 세우고 달성하세요!",
    "새해에는 더 높은 곳을 향해 도전하세요!",
    "새해에는 자신을 믿고 목표를 이루세요!",
    "새해에는 더 큰 성취를 이루세요!",
    "새해에는 끊임없이 도전하세요!",
    "새해에는 목표를 향해 한 걸음 더 나아가세요!",
    "새해에는 더 큰 꿈을 꾸고 이루세요!",
    "새해에는 새로운 시작을 맞이하세요!",
    "새해에는 더 큰 목표를 세우고 달성하세요!",
    "새해에는 더 많은 성취를 이루세요!",
    "새해에는 더 큰 도전을 향해 나아가세요!",
    "새해에는 목표를 향해 힘차게 나아가세요!",
    "새해에는 꿈을 이루기 위해 최선을 다하세요!",
    "새해에는 새로운 목표를 세우고 달성하세요!",
    "새해에는 더 높은 곳을 향해 도전하세요!",
    "새해에는 자신을 믿고 목표를 이루세요!",
    "새해에는 더 큰 성취를 이루세요!",
    "새해에는 끊임없이 도전하세요!",
    "새해에는 목표를 향해 한 걸음 더 나아가세요!",
    "새해에는 더 큰 꿈을 꾸고 이루세요!",
    "새해에는 새로운 시작을 맞이하세요!",
    "새해에는 더 큰 목표를 세우고 달성하세요!",
    "새해에는 더 많은 성취를 이루세요!",
    "새해에는 더 큰 도전을 향해 나아가세요!",
    "새해에는 목표를 향해 힘차게 나아가세요!",
    "새해에는 꿈을 이루기 위해 최선을 다하세요!",
    "새해에는 새로운 목표를 세우고 달성하세요!",
    "새해에는 더 높은 곳을 향해 도전하세요!",
    "새해에는 자신을 믿고 목표를 이루세요!",
    "새해에는 더 큰 성취를 이루세요!",
    "새해에는 끊임없이 도전하세요!",
    "새해에는 목표를 향해 한 걸음 더 나아가세요!",
    "새해에는 더 큰 꿈을 꾸고 이루세요!",
    "새해에는 새로운 시작을 맞이하세요!",
    "새해에는 더 큰 목표를 세우고 달성하세요!",
    "새해에는 더 많은 성취를 이루세요!",
    "새해에는 더 큰 도전을 향해 나아가세요!",
    "새해에는 목표를 향해 힘차게 나아가세요!",
    "새해에는 꿈을 이루기 위해 최선을 다하세요!",
    "새해에는 새로운 목표를 세우고 달성하세요!",
    "새해에는 더 높은 곳을 향해 도전하세요!",
    "새해에는 자신을 믿고 목표를 이루세요!",
    "새해에는 더 큰 성취를 이루세요!",
    "새해에는 끊임없이 도전하세요!",
    "새해에는 목표를 향해 한 걸음 더 나아가세요!",
    "새해에는 더 큰 꿈을 꾸고 이루세요!",
    "새해에는 새로운 시작을 맞이하세요!"
};

  DrawImageAsync img-chatbox 에서 , Chatbox를 그리니 이미지가 뭉개집니다. 이미지가아닌 도형을 말풍선처럼 그려 글자 크기에 맞게 표현하는것로 개선

public async ValueTask Render(SceneContext game, Canvas2DContext context)
{
    string NameText = Name;

    if (!string.IsNullOrEmpty(ChatMessage))
    {
        NameText = NameText + "-" + ChatMessage;
        ChatViewTime--;
    }

    if (ChatViewTime < 0)
    {
        ChatMessage = string.Empty;
    }

    await context.SaveAsync();

    // 닉네임
    await context.SetFontAsync("14px 바탕체");
    await context.SetFillStyleAsync("Blue");
    await context.FillTextAsync(Name, _transform.Local.Position.X + 10, _transform.Local.Position.Y + 75);

    if (!string.IsNullOrEmpty(ChatMessage))
    {
        // 채팅 Box
        int padding = 10;
        int dynamicWidth = 50 + ((ChatMessage.Length - 3) * 15);
        int dynamicHeight = 50;

        float boxX = _transform.Local.Position.X + 20;
        float boxY = _transform.Local.Position.Y - 40;

        // 말풍선 배경
        await context.SetFillStyleAsync("White");
        await context.FillRectAsync(boxX, boxY, dynamicWidth, dynamicHeight);

        // 말풍선 테두리
        await context.SetStrokeStyleAsync("Black");
        await context.StrokeRectAsync(boxX, boxY, dynamicWidth, dynamicHeight);

        // 채팅 메시지
        await context.SetFillStyleAsync("Black");
        await context.FillTextAsync(ChatMessage, boxX + padding, boxY + padding + 20);
    }

    if (_isMine)
    {
        // 추가적인 렌더링 로직이 필요하다면 여기에 작성
    }

    await context.RestoreAsync();
}



사각형이 아니라 말풍선처럼 타워에 꼭지점도 표현

public async ValueTask Render(SceneContext game, Canvas2DContext context)
{
    string NameText = Name;

    if (!string.IsNullOrEmpty(ChatMessage))
    {
        NameText = NameText + "-" + ChatMessage;
        ChatViewTime--;
    }

    if (ChatViewTime < 0)
    {
        ChatMessage = string.Empty;
    }

    await context.SaveAsync();

    // 닉네임
    await context.SetFontAsync("14px 바탕체");
    await context.SetFillStyleAsync("Blue");
    await context.FillTextAsync(Name, _transform.Local.Position.X + 10, _transform.Local.Position.Y + 75);

    if (!string.IsNullOrEmpty(ChatMessage))
    {
        // 채팅 Box
        int padding = 10;
        int dynamicWidth = 50 + ((ChatMessage.Length - 3) * 15);
        int dynamicHeight = 50;

        float boxX = _transform.Local.Position.X + 20;
        float boxY = _transform.Local.Position.Y - 40;
        float tailWidth = 10;
        float tailHeight = 10;

        // 말풍선 배경
        await context.BeginPathAsync();
        await context.MoveToAsync(boxX, boxY);
        await context.LineToAsync(boxX + dynamicWidth, boxY);
        await context.LineToAsync(boxX + dynamicWidth, boxY + dynamicHeight);
        await context.LineToAsync(boxX + dynamicWidth / 2 + tailWidth, boxY + dynamicHeight);
        await context.LineToAsync(boxX + dynamicWidth / 2, boxY + dynamicHeight + tailHeight);
        await context.LineToAsync(boxX + dynamicWidth / 2 - tailWidth, boxY + dynamicHeight);
        await context.LineToAsync(boxX, boxY + dynamicHeight);
        await context.ClosePathAsync();
        await context.SetFillStyleAsync("White");
        await context.FillAsync();

        // 말풍선 테두리
        await context.SetStrokeStyleAsync("Black");
        await context.StrokeAsync();

        // 채팅 메시지
        await context.SetFillStyleAsync("Black");
        await context.FillTextAsync(ChatMessage, boxX + padding, boxY + padding + 20);
    }

    if (_isMine)
    {
        // 추가적인 렌더링 로직이 필요하다면 여기에 작성
    }

    await context.RestoreAsync();
}


여기서 작동하는 핵심 서버 로직은 액터모델로 구현되었으며 자바/코틀린/닷넷 동일컨셉으로 모두 이용가능하며 관련기술은 다음 공간에서 확인할수 있습니다.