굶지마 어인왕 HUD 모드

뾰족머리삼돌이·2024년 12월 29일
0

기타

목록 보기
1/7
post-thumbnail

굶지마(Don't Starve Together)라는 게임의 모드를 만들었던 내용을 기록하는 포스팅이다.
모드 가이드는 Klei 공식포럼에서 정보를 얻을 수 있다.

작성자 본인도 주먹구구식으로 다른 모드파일을 참고해가며 제작했기에 자세한 내용은 정확히 모른다.
따라서, 모드를 제작하며 파악한 부분과 느낀 점만 간단히 작성할 예정이다.

GitHub 소스코드

실제 게임플레이를 진행하면서 wurt라는 캐릭터의 불편함을 개선하기 위해 모드를 제작했다.

시작

굶지마는 Lua라는 언어로 코딩되어있으며, \steamapps\common\Don't Starve Together 경로로 찾아가면 게임의 스크립트, 애니메이션, 이미지 파일 등을 직접 확인할 수 있다.

애니메이션 파일을 압축해제하기 위해서는 ktools라는 프로그램을 이용했다.
해당 프로그램의 위치에 압축해제한 파일( anim.bin, build.bin, tex파일 )들을 위치시키고 command 창에서 아래 명령어를 입력하면 된다.

krane anim.bin build.bin {해제된 파일이 담길 폴더명}

모드를 제작하기위해 필요한 최소한의 파일은 modmain.luamodinfo.lua다.
해당 파일을 \steamapps\common\Don't Starve Together\mods 경로에 모드폴더를 제작하고, 포함시키면 인게임에서 모드가 추가된 모습을 확인할 수 있다.

modinfo.lua

modinfo.lua모드의 설정정보를 작성하는 파일이다.
모드이름, 모드설명, 버전, 작성자, 굶지마 싱글 및 dlc 등의 호환여부, 모드 유형 등을 설정할 수 있다.

여기에 작성한 아이콘 파일은 해당하는 경로에 반드시 존재해야한다.

name = "이름"
description = "설명"
author = "제작자"
version = "버전"
forumthread = ""

-- 인게임에서 보여지는 모드 아이콘 정보
icon_atlas = "modicon.xml" -- tex파일 내의 이미지에 대한 영역배치 정보
icon = "modicon.tex" -- 아이콘 파일

all_clients_require_mod = true -- 서버 모드
client_only_mod = false -- 클라이언트 모드

-- 호환여부 체크
dont_starve_compatible = false -- 굶지마 싱글
reign_of_giants_compatible = false  -- 거인의 군림 dlc
shipwrecked_compatible = false -- 난파선 dlc
dst_compatible = true -- 굶지마 투게더

-- API 버전 
-- scripts의 mods.lua 파일에서 확인할 수 있으며, 굶지마 투게더의 경우 10
api_version = 10 


-- 모드 옵션 설정정보
configuration_options =
{
    {	-- 옵션
        name = "mod파일에서 사용할 옵션 식별자",
        label = "플레이어에게 보여질 옵션명",
        hover = "플레이어가 옵션에 마우스를 올릴 때, 표시될 설명",
        options =   { 	
        				-- 옵션 종류 및 값
                        -- description : 플레이어에게 보여줄 옵션
                        -- data : 실제 해당되는 값
                        {description = "Merm", data = true},
                        {description = "All", data = false},
                    },
        default = true
    }
}

modmain.lua

만들고자하는 모드의 실질적인 구현이 작성되는 파일이다.

해당 파일에서 GetModConfigData("name") 의 형식으로 modinfo.lua에 작성한 configuration_options 정보를 받아올 수 있다.

실제 게임코드를 작성하기 위해서는 기존의 게임코드들을 해석하는게 중요하다.
월드 정보를 관리하는 TheWorld, 플레이어 정보를 관리하는 ThePlayer, PrefabClass처럼 게임의 소스코드를 해석하며 어떤 의미를 가지고 어떻게 동작하는지를 알아내야한다.

본인같은 경우에도 초기 경로에 포함된 scripts 폴더의 파일들과 다른 모드의 파일들을 참고하면서 만들었다.

변변찮은 팁을 작성하자면 몇 가지 객체가 관리하는 내용은 아래와 같다.

TheWorld : 세계 정보를 관리
ThePlayer : 플레이어 정보를 관리
TheSim : 엔진, 시간흐름, 이벤트 등을 관리
TheNet : 클라이언트와 서버간의 통신, RPC 통신등을 관리

2가지 헤맸던 부분만 살펴보자

서버 Shard간 정보 공유

굶지마의 경우에는 지상과 동굴이 존재하는데, 이는 각각 별개의 서버로 동작한다.
즉, 지상서버와 동굴서버가 따로 존재하고 플레이어는 각 서버를 이동하는 방식이다.

문제는 어인왕의 상태정보를 세계(서버)단위로 관리한다는 것이었다.
플레이어가 다른 서버로 넘어가게되면 해당 서버에는 어인왕이 존재하지 않게되고, 기존의 TheWorld.components.mermkingmanager:GetKing()을 통해 객체를 얻어올 수 없게된다.

-- player_classified prefab 확장
AddPrefabPostInit("player_classified", function(inst)
	
    ...
    
    -- 프레임마다 mermkingmanager의 상태값으로 네트워크 변수를 갱신
    inst:DoPeriodicTask(0, function()
        local mermkingmanager = GLOBAL.TheWorld.components.mermkingmanager
        if mermkingmanager ~= nil then
            
            -- 현재 플레이어 Shard와 어인왕의 Shard가 동일한 위치인지 확인하여 분기
            if mermkingmanager:HasKingLocal() then
                local king = mermkingmanager:GetKing()
                inst.net_mermking_hunger_max:set(king.components.hunger.max)
                inst.net_mermking_hunger_current:set(king.components.hunger.current) 
                inst.net_mermking_health_regen:set(king.components.health.regen ~= nil)
                inst.net_mermking_health_current:set(king.components.health.currenthealth)
            elseif mermkingmanager:HasKingAnywhere() then
                -- 반대쪽 Shard(동굴 or 지상)로 시그널 전달
                -- 두번째 인자가 nil인 경우, 연결된 모든 Shard로 요청을 전송
                SendModRPCToShard(GetShardModRPC(modname, "mermking_update"), nil, nil, nil, nil, nil) 
            else
                -- 어인왕이 존재하지 않을 경우, 기본값으로 초기화
                inst.net_mermking_hunger_max:set(TUNING.MERM_KING_HUNGER)
                inst.net_mermking_hunger_current:set(0)
                inst.net_mermking_health_regen:set(false)
                inst.net_mermking_health_current:set(0)
            end
        end
    end)
end)

-- shard(동굴, 지상) 간 데이터 교환을 위한 원격 프로시저 핸들러
-- RPC 시그널을 생성한 Shard와 동일한지 확인하고, 상태값의 유무에 따라 수행할 동작 분기
-- shardId는 RPC 요청을 생성한 Shard를 의미
AddShardModRPCHandler(modname, "mermking_update", function(shardId, hunger_max, hunger_current, health_regen, health_current)
    if GLOBAL.TheShard:GetShardId() ~= tostring(shardId) then
        if hunger_max ~= nil and hunger_current ~= nil and health_regen ~= nil and health_current ~= nil then
            player_inst.net_mermking_hunger_max:set(hunger_max)
            player_inst.net_mermking_hunger_current:set(hunger_current)
            player_inst.net_mermking_health_regen:set(health_regen)
            player_inst.net_mermking_health_current:set(health_current)
        else
            local king = GLOBAL.TheWorld.components.mermkingmanager:GetKing()
            GLOBAL.TheWorld:DoTaskInTime(0, function()
                SendModRPCToShard(GetShardModRPC(modname, "mermking_update"), shardId, 
                    king.components.hunger.max, 
                    king.components.hunger.current, 
                    king.components.health.regen ~= nil, 
                    king.components.health.currenthealth
                )
            end) 
        end
    end
end)

이 부분에서 좀 헤맸는데, 결론만 말하자면 RPC를 통해 해결했다.

다행히도 mermkingmanager 객체에 어인왕이 존재하는지를 판단할 수 있는 HasKingLocaL()HasKingAnywhere() 함수가 존재했다. 이를통해 만약, 현재 서버(Shard)에 어인왕이 존재하지 않는다면 반대편 서버(Shard)로 요청을 보내 어인왕 정보를 받아오는 RPC를 작성했다.

클라이언트와 서버간 정보 동기화

어인왕의 허기정보를 플레이어에게 전달하는 것도 파악하는데 시간이 꽤 걸렸다.

-- 플레이어 HUD 상태 표현 클래스 확장 
-- self는 출력 시, Status 객체로 표현됨
AddClassPostConstruct("widgets/statusdisplays", function(self)
end)

-- player_classified prefab 확장
AddPrefabPostInit("player_classified", function(inst)
end)

widgets/statusdisplays는 클라이언트에서만 동작하지만,
player_classified는 클라이언트와 서버 양쪽에서 모두 동작한다.

뱃지 출력을 위해서는 클라이언트 쪽에서 어인왕의 정보를 파악해야하지만, 어인왕 정보는 서버측에서 파악할 수 있다.
따라서, 서버에서 파악한 정보를 클라이언트에게 동기화하는 작업이 필요했다.

이를 위해서 net_* 로 초기화가 가능한 네트워크 변수를 이용했다.

네트워크 변수와 관련된 설명은 이 곳에서 확인할 수 있다.

inst.mermking_hunger_current = 0
inst.net_mermking_hunger_current = GLOBAL.net_float(inst.GUID, "mermking_hunger_current", "mermking_hunger_current_dirty")
inst:ListenForEvent("mermking_hunger_current_dirty", function(inst)
    inst.mermking_hunger_current = inst.net_mermking_hunger_current:value()
end)

mermking_hunger_currentnet_mermking_hunger_current와 같이 멤버변수와 네트워크 변수를 생성했다.
멤버변수는 클라이언트가 접근하기 위해 사용되며, 네트워크 변수는 서버와 클라이언트의 값을 동기화하는데 사용된다.

inst:DoPeriodicTask(0, function()
        local mermkingmanager = GLOBAL.TheWorld.components.mermkingmanager
        if mermkingmanager ~= nil then
            
            -- 현재 플레이어 Shard와 어인왕의 Shard가 동일한 위치인지 확인하여 분기
            if mermkingmanager:HasKingLocal() then
                local king = mermkingmanager:GetKing()
    			inst.net_mermking_hunger_current:set(king.components.hunger.current) 
       	-- ...
end)

예를들어, 서버측에서 위 형태로 멤버변수의 값을 변경했다고 가정해보자.
서버측에서는 어인왕 정보를 얻을 수 있으므로 정상적으로 값을 조회하고 반영할 것이다.

서버에서만 동작해야하므로, mermkingmanager에 대한 null체크를 해줬다.

네트워크 변수의 값이 변경되는 순간 mermking_hunger_current_dirty 이벤트가 발생한다.
클라이언트는 ListenForEvent를 통해 해당 이벤트를 수신하고 있으므로, 네트워크 변수의 값이 클라이언트의 멤버변수 mermking_hunger_current에 반영된다.

모드 배포

굶지마는 스팀 창작마당을 통해 모드를 배포할 수 있다.

스팀 라이브러리에서 도구를 체크하면 Don't Starve Mod Tools 라는 도구가 나타난다.
이는 애니메이션 파일들을 zip으로 압축하거나, 모드를 창작마당에 게시하는데 사용된다.

애니메이션 파일 압축은 모드폴더에 exported라는 폴더를 생성하고,
이미지와 scml파일을 넣어두면 굶지마를 실행할때 자동으로 이뤄진다.

애니메이션 관련 내용은 이 곳을 참고했다.

해당 도구를 직접 실행하면 이런 UI가 표시된다.


Add를 통해 모드폴더와 preview 이미지, 태그 등을 설정하고 Publish!를 클릭하면 창작마당에 업로드된다.
모드 설명이나 패치노트는 창작마당에서 추가작성이 가능하므로, 이 시점에서 반드시 작성하지 않아도 된다.

후기

Lua라는 언어자체를 처음 접해봤고, 소스코드 내에 동작하는 객체들을 몰랐기 때문인지 처음에는 많이 헤맸다.
특히, 객체들의 초기화 시점이나 서버와 클라이언트로 구분되는 상태관리, 문법 등 등을 배우는데는 시간이 좀 걸렸다.

아이콘 생성 및 수정에는 gimp라는 프로그램을 이용했는데, 처음으로 이미지의 누끼(외각선)을 따봤다.
원본 이미지는 굶지마 게임폴더의 애니메이션 파일에서 얻었다.

그리고, 애니메이션 파일생성에는 Spriter라는 프로그램을 이용했다.
2D 애니메이션 파일을 생성하는데 사용된다는 내용을 포럼에서 확인했기 때문이다.

소스코드들을 파악해가며 원하는 바를 구현해내는 모든 과정이 재밌었기 때문에 다음에도 필요한 모드가 생각나면 만들어볼 생각이다.

0개의 댓글