본문 바로가기
Learn

전투 애니메이션 로직 - (4) 방어기능 적용 , 공격시 이동 제한, 카메라고정

by Roblox_개발자 2021. 4. 28.

작성자 : CHAN

최종 업데이트 : 2021.04.28

기타 : 이전 글과 이어서 작성되있음 (이전글 : roblox-blog.tistory.com/79 

이 예제에서는 상대방의 공격을 막을 수 있는 방어 기능을 추가한다.
또, 공격시에는 움직일수 없도록 이동을 제한하는 기능도 추가한다.
카메라 고정은 나중에 추가할 예정

주석에 서술한 내용이 많으므로 주석을 잘 읽어볼 것 

사전준비

- 이전글 : 방어하기 까지 되있는 코드에서 추가로 작성하는 부분들이기 때문에, 이전까지의 내용을 잘 따라왔어야 코드 추가 및 적용 가능하다.

-  로블록스에 저장된 방어시 나는 소리(효과음)

 

사전공부

1. tween
2. Humanoid
3. CFrame

 

1. 방어기능 적용하는 타이밍

StarterPlayer / StarterCharacterScripts / Combat2 / CombatSystem2(Script)

-1. 방어하기 기능 적용

방어하기 기능을 적용시키는 타이밍을 생각해보자 . 공격은  Hitbox가 상대에게 닿았을때 실행된다.

Hitbox가 상대에게 닿았을 때, 그 상대가 방어중이라는 변수를 가지고 있으면, 방어가 실행되게 만들면된다.

이전 글에서 방어기능을 만들 때, 방어 애니메이션이 실행될때 (Q버튼 클릭 시) 
Humanoid 안에 blockAction 이라는 boolean값을 만들어주고( 인스턴스 생성 ),
애니메이션이 끝날 시 그 값을 제거해주었다. ( 인스턴스 파괴 )  
(모르겠으면 BlockingSystem2 다시 보고 오기 링크)


즉, Hitbox가 상대방에 닿는 타이밍인 
CombatSystem2 의 " Hitbox.Touched:Connect(function(Hit)" 부분으로 가자
   Hitbox.Touched:Connect(function(Hit) --Hit은 Hit가 건드린 객체
        
        --Hit은 닿은 플레이어의 Part, basePlate 등 hitbox와 닿은 모든 객체를 출력한다.
        --print(Hit)
        
        --basepart가 아니면 플레이어가 아니기 때문에 1차적으로 걸러준다.
        if Hit:IsA("BasePart") then
            if not Hit:IsDescendantOf(Character) then -- Hit의 parent들에 chrarcter(나)가 없을 경우 , Hit이 자기 자신이 아닌 경우 
                local enemyHumanoid = Hit.Parent:FindFirstChild("Humanoid")  -- 때린 적의 Humanoid를 가져온다. (Hit.Parent = Character)
                local enemyHumrp = Hit.Parent:FindFirstChild("HumanoidRootPart")	-- 때린 적의 HumanoidRootPart를 가져온다.  
  
  --상대의 Humanoid, RootPart가 존재하는 경우 / Hitbox가 상대방 플레이어를 때린 경우
                if enemyHumanoid and enemyHumrp then
                   
                    
                    warn("상대방 캐릭터를 때렸다.")
                    
                    --[[방어기능 적용하기 ( 추가되는 코드입니다!!!!!!! )]]--
                    
                    --방어하기를 누르면 해당 캐릭 Humanoid 안에 "blockAction" 이라는 값이 생성됨 (BlockingSystem2참고)
                    local blockAction = enemyHumanoid:FindFirstChild("blockAction")
                    
                    -- blockAction값이 있으면 방어중
                    if blockAction then
                        print("상대방 방어 중")

                        Hitbox:Destroy()	-- Hitbox제거 (제거시키지 않으면 Touched 이벤트가 여러번 발생. 잘 모르겠으면 주석처리 후 실행 해볼 것)
                        HitEffect(enemyHumrp)	-- 나중에 BlockEffect() 함수로 변경 예정
                        Knockback(enemyHumrp)
                        PlayBlockSound(Player)
                        
                    -- blockAction값이 없으면 타격성공
                    else
                  
					-- 기존에 있던 타격시 코드 들어갈 부분..
                    end
상대방이 Humanoid인지 확인하는 부분인 if enemyHumanoid and enemyHumrp then 
이 부분에 

1. 방어하기 때 생성한 blockAction값이 상대 Humanoid 안에 있는지 체크하고
2. blockAction값이 있으면 방어중, 없으면 타격 중인 if문을 추가한다.
3. 타격중인 부분 (else) 부분에는 기존 타격하는 코드를 넣는다.

방어중일때는 데미지가 들어가지 않도록 처리하고 나머지 부분들 
1. Hitbox제거 
2. 이펙트
3. 넉백 
기능은 유지한다.

+방어시 사운드 추가한다.

 

-2. 방어시 사운드 추가

-- 방어시 소리 플레이시키는 함수
local function PlayBlockSound(Player)
    local sound = Instance.new("Sound")
    sound.SoundId = "rbxassetid://6725457452"
    sound.Parent = Player.Character.HumanoidRootPart
    sound:Play()
    game.Debris:AddItem(sound,1)
end
Loacl함수 부분에  방어시 나는 소리를 미리 업로드 하고, 함수를 추가시킨다.
(타격시 나는 소리 함수와 동일 - PlayHitSound()) 

 

 

혼자서 테스트해보기 어려우니, 게임을 로블록스에 업로드한 뒤 지인과 테스트하면 된다. 
막기했을때 피가 안달고, 막는 소리가 난다면 성공이다. 

 

 

2. 공격시 이동 제한 & 카메라 고정

여기까지 잘 따라왔으면 기본적인 전투기능은 되있을 것이다.
하지만 뭔가 어색한 부분이 있다.

1. 공격시 이동 가능해서 타격감이 잘 안삼
2. 공격시 나오는 이펙트가 측면에서 나오는 경우 

 

-1. 공격시 이동 제한

StarterPlayer / StarterCharacterScripts / Combat2 
local TweenService = game:GetService("TweenService")
local Humrp = Character:WaitForChild("HumanoidRootPart")




-- 공격시 플레이어를 앞으로 전진 & 이동 불가능하게 만드는 함수
local function thrust()

    -- 이동속도 & 점프 disable
    Humanoid.WalkSpeed = 0
    Humanoid.JumpPower = 0

    local goal = {}   -- 플레이어를 이동시킬 위치를 담을 테이블 변수
    local newCFrame = CFrame.new(0, 0, -3)	
    goal.CFrame = Humrp.CFrame * newCFrame	-- 현재 플레이어의 HumanoidRootPart 위치에서 Z축으로 -3 stud한 위치

    local info = TweenInfo.new(.3)	-- 0.3초 동안 진행

    -- (적용시킬 객체, tween정보, 목표지점)
    local tween = TweenService:Create(Humrp, info, goal) -- Humrp를 goal위치로 0.3초만에 이동시키는 tween 객체 생성
    tween:Play()	-- tween 플레이

    wait(0.5)	-- 대략적인 애니메이션 하나당 걸리는 시간. 추후에 값 조정이 필요.

    -- 이동속도 & 점프 able
    Humanoid.WalkSpeed = 22
    Humanoid.JumpPower = 50
end
공격기능 클라이언트 부분 ( Combat2) 

1. 변수&인스턴스 부분에 TweenService과 HumanoidRootPart 를 추가한다.

2. 공격시 플레이어를 앞으로 전진 & 이동 불가능하게 만드는 함수를 추가한다 

3. 만든 함수를 추가한다. 

CombatSystem2의 넉백시키는 함수 local function Knockback(HumanoidRootPart) 과 유사하다.
CFrame과 Tween 기능을 잘 모르면 따로 공부

 

 

--유저의 입력을 받았을 때 일어나는 함수 
UserInputService.InputBegan:Connect(function(input)
    
    --유저가 어떤 키를 누르는지 다 알 수 있게 해준다.
    --print(input.UserInputType) 
    
    --사용자가 마우스 왼쪽클릭 한 경우
    if input.UserInputType == Enum.UserInputType.MouseButton1  then
        
        --애니메이션 실행중이 아닌 경우
        if not debounce then
            debounce = true
            prev = current		-- 이전 클릭 시간
            current = tick()	-- 현재 클릭 시각 
            
            local gap = current - prev
            warn("Gap : "..gap)     --마우스 클릭 간격 차이
            
            -- 콤보 계산 로직
            -- 콤보 수마다 코드가 달라져야 하므로 나중에 변경되야함 지금은 2콤보 기준으로 작성
            -- 클라에서 서버로 combo라는 변수값을 보내서 1이면 콤보1 실행, 2면 콤보 2 실행 
            if gap > 0.8 then	-- 이전 클릭과의 시간 gap이 0.8초 초과면 콤보 해제
                combo = 1
                HitEvent:FireServer(combo)
                combo = combo + 1
            elseif combo == 1 and gap <= 0.8 then
                HitEvent:FireServer(combo)
                combo = combo + 1
            elseif combo == 2 then
                HitEvent:FireServer(combo)
                combo = 1
            end
            
            
            thrust()
            --애니메이션 끝난 후 애니메이션진행중이 아니라고 상태 저장
            debounce = false
        end
    end
end)
thrust()함수를 마우스 클릭했을때 ( 공격했을때 ) 에 추가시킨다.

 

-2. 카메라 고정 ( 갯앰프드처럼 )

추가될 예정입니다.

 

 

 

 

 

전체코드

-1. combat2 ( 공격 클라이언트 코드 )

--[[전투 관련 클라이언트 코드]]--
-- 공부해야할 사항: TweenService, RemoteEvent, CFrame, UserInputService

local Player = game.Players.LocalPlayer                  --goyou_chan
local Character = Player.Character                       --goyou_chan
local Humanoid = Character:FindFirstChild("Humanoid")    --Humanoid
local UserInputService = game:GetService("UserInputService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local HitEvent = ReplicatedStorage:WaitForChild("HitEvent")

local TweenService = game:GetService("TweenService")
local Humrp = Character:WaitForChild("HumanoidRootPart")


local combo = 1   -- 진행시킬 콤보
local current = 0	-- 현재 마우스 클릭 시간
local prev = 0		-- 이전 마우스를 클릭 시간
local debounce = false	-- 애니메이션이 실행중인지 판별하는 변수 (애니메이션 중복 플레이 못하도록 하는데 사용)


-- 공격시 플레이어를 앞으로 전진 & 이동 불가능하게 만드는 함수
local function thrust()

    -- 이동속도 & 점프 disable
    Humanoid.WalkSpeed = 0
    Humanoid.JumpPower = 0

    local goal = {}   -- 플레이어를 이동시킬 위치를 담을 테이블 변수
    local newCFrame = CFrame.new(0, 0, -3)	
    goal.CFrame = Humrp.CFrame * newCFrame	-- 현재 플레이어의 HumanoidRootPart 위치에서 Z축으로 -3 stud한 위치

    local info = TweenInfo.new(.3)	-- 0.3초 동안 진행

    -- (적용시킬 객체, tween정보, 목표지점)
    local tween = TweenService:Create(Humrp, info, goal) -- Humrp를 goal위치로 0.3초만에 이동시키는 tween 객체 생성
    tween:Play()	-- tween 플레이

    wait(0.5)	-- 대략적인 애니메이션 하나당 걸리는 시간. 추후에 값 조정이 필요.

    -- 이동속도 & 점프 able
    Humanoid.WalkSpeed = 22
    Humanoid.JumpPower = 50
end





--유저의 입력을 받았을 때 일어나는 함수 
UserInputService.InputBegan:Connect(function(input)

    --유저가 어떤 키를 누르는지 다 알 수 있게 해준다.
    --print(input.UserInputType) 

    --사용자가 마우스 왼쪽클릭 한 경우
    if input.UserInputType == Enum.UserInputType.MouseButton1  then

        --애니메이션 실행중이 아닌 경우
        if not debounce then
            debounce = true
            prev = current		-- 이전 클릭 시간
            current = tick()	-- 현재 클릭 시각 

            local gap = current - prev
            warn("Gap : "..gap)     --마우스 클릭 간격 차이

            -- 콤보 계산 로직
            -- 콤보 수마다 코드가 달라져야 하므로 나중에 변경되야함 지금은 2콤보 기준으로 작성
            -- 클라에서 서버로 combo라는 변수값을 보내서 1이면 콤보1 실행, 2면 콤보 2 실행 
            if gap > 0.8 then	-- 이전 클릭과의 시간 gap이 0.8초 초과면 콤보 해제
                combo = 1
                HitEvent:FireServer(combo)
                combo = combo + 1
            elseif combo == 1 and gap <= 0.8 then
                HitEvent:FireServer(combo)
                combo = combo + 1
            elseif combo == 2 then
                HitEvent:FireServer(combo)
                combo = 1
            end


            thrust()
            --애니메이션 끝난 후 애니메이션진행중이 아니라고 상태 저장
            debounce = false
        end
    end
end)

 

-2. combatSystem2 ( 공격 서버 코드 )

--전투 관련 서버 코드

local Player = game:GetService("Players").LocalPlayer
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local HitEvent = ReplicatedStorage:WaitForChild("HitEvent")
local Animations = script:WaitForChild("Animations")

local Debris = game:GetService("Debris")
local EnemyAnims = script:WaitForChild("EnemyAnims")

--local Meshes = script:WaitForChild("Meshes")
local TweenService = game:GetService("TweenService") 


local Damage = 10

------------------------------------------------------

--[[애니메이션 모음 테이블 ]]--
local anims = {  -- 유저 공격 순서(콤보)
    --Animations:WaitForChild("Taekwon2"),
    Animations:WaitForChild("Taekwon3"),
    Animations:WaitForChild("Taekwon4")
}


-- 유저가 타격할 때 사용한 부위. ( 콤보순서랑 순서가 맞아야함 + 애니메이션 만들때 어느 부위인지 기억)
-- 해당 부위에 HitBox가 생성될 예정
local limbs = {
    "LeftFoot",
    "RightFoot"
}


local eAnims = {  -- 적 맞는 모션 순서 (콤보 순서랑 대칭을 이뤄야 한다.)
    EnemyAnims:WaitForChild("Hitted1"),
    EnemyAnims:WaitForChild("Hitted2")
}



-------------[[로컬 함수]]-----------------------------
--히트박스를 생성하는 함수 ( 애니메이션 실행될때 바로 실행 )
local function CreateHitbox(Player,combo)
    local Character = Player.Character
    local Limb = Character:WaitForChild(limbs[combo]) -- combo에 따라 어느 부위로 때렸는지 가져온다.
    
    --플레이어 이름의 폴더를 생성한다. 
    local folder = Instance.new("Folder", Character) 
    folder.Name = Player.Name
    
    -- 객체를 복사해준다. (복사해주는 이유는 계속 생성됐다가 사라지는걸 반복해야 하기 때문에 본 객체는 건들면 안된다.)
    local Hitbox = ReplicatedStorage:WaitForChild("Hitbox"):Clone()
    
    Hitbox.CFrame = Limb.CFrame   -- Hitbox의 위치를 타격할때 사용한 부위와 일치시킨다. 
    Hitbox.Parent = folder      -- ?? 왜 parent 지정해주는지 모르겠음
    Debris:AddItem(folder, .5)  -- Hitbox를 0.5초 후 사라지게 한다.

    local weld = Instance.new("WeldConstraint")
    weld.Part0 = Hitbox   -- Part0은 용접할 첫번째 Part ( 히트박스와 플레이어가 맞닿는 부위 )
    weld.Part1 = Limb     -- Part1은 용접할 두번째 Part ( 플레이어의 부위 )
    weld.Parent = workspace
    
    Debris:AddItem(weld, .5) -- 마찬가지로 용접부위도 0.5초후 사라지게 함

    return Hitbox
    
    
end


-- 방어시 소리 플레이시키는 함수
local function PlayBlockSound(Player)
    local sound = Instance.new("Sound")
    sound.SoundId = "rbxassetid://6725457452"
    sound.Parent = Player.Character.HumanoidRootPart
    sound:Play()
    game.Debris:AddItem(sound,1)
end


-- 타격 시 effect 생성시키는 함수
-- 파라미터는 맞은 플레이어의 휴머노이드의 rootPart이다. 
local function HitEffect(Humnrp)
    -- 타격시 이펙트 주기
    
    --workspace에 Hit effect 폴더를 생성한다.
    local effects = Instance.new("Folder", workspace)
    effects.Name = "Hit effect"
    
    --미리 준비해놓은 HitEffect part를 복제하고 상대방의 rootpart 포지션에 생성한다. 
    local hiteffect = ReplicatedStorage:WaitForChild("HitEffect"):Clone()
    hiteffect.Position = Humnrp.Position 
    hiteffect.Parent = effects         -- 위에서 만든 폴더 안에 넣는다. (정리를 위해) 
    
    -- effects 폴더를 0.2초후 제거
    game.Debris:AddItem(effects,0.2) 
end


--넉백시키는 함수
local function Knockback(HumanoidRootPart)
    local goal = {}
    local newCFrame = CFrame.new(0, 0, 3)
    goal.CFrame = HumanoidRootPart.CFrame * newCFrame

    local info = TweenInfo.new(.2)
    local tween = TweenService:Create(HumanoidRootPart, info, goal)
    tween:Play()
end


-- 타격시 소리 플레이시키는 함수
local function PlayHitSound(Player)
    local sound = Instance.new("Sound")
    sound.SoundId = "rbxassetid://131237241"
    sound.Parent = Player.Character.HumanoidRootPart
    sound:Play()
    game.Debris:AddItem(sound,1)
end



--[[실제 전투 로직이 구현 되는 부분]]--
--마우스 클릭 했을때 HitEvent받는 붑누 / 자동으로 Player 가 첫번째 파라미터에 추가됨
HitEvent.OnServerEvent:Connect(function(Player, combo)
    
    --print("마우스 왼쪽클릭시 서버에서 받음")
    
    local Character = Player.Character
    local Humanoid = Character:FindFirstChild("Humanoid")
    local Humrp = Character:WaitForChild("HumanoidRootPart")
    local Animator = Humanoid:FindFirstChildOfClass("Animator")
    
    --받은 콤보변수값에 따라 다른 애니메이션을 실행시킨다.
    local attack = Animator:LoadAnimation(anims[combo])
    attack:Play()
    
    
    -- Hitbox 생성
    local Hitbox = CreateHitbox(Player, combo)	
    
    
    --Hitbox에 터치 이벤트 추가
    --Hitbox에 객체가 닿으면 Connect안의 함수가 발생. 파라미터는 Hitbox를 건드린 객체
    Hitbox.Touched:Connect(function(Hit) --Hit은 Hit가 건드린 객체
        
        --Hit은 닿은 플레이어의 Part, basePlate 등 hitbox와 닿은 모든 객체를 출력한다.
        --print(Hit)
        
        --basepart가 아니면 플레이어가 아니기 때문에 1차적으로 걸러준다.
        if Hit:IsA("BasePart") then
            if not Hit:IsDescendantOf(Character) then -- Hit의 parent들에 chrarcter(나)가 없을 경우 , Hit이 자기 자신이 아닌 경우 
                local enemyHumanoid = Hit.Parent:FindFirstChild("Humanoid")  -- 때린 적의 Humanoid를 가져온다. (Hit.Parent = Character)
                local enemyHumrp = Hit.Parent:FindFirstChild("HumanoidRootPart")	-- 때린 적의 HumanoidRootPart를 가져온다.
                
                
                --상대의 Humanoid, RootPart가 존재하는 경우 / Hitbox가 상대방 플레이어를 때린 경우
                if enemyHumanoid and enemyHumrp then
                   
                    
                    warn("상대방 캐릭터를 때렸다.")
                    
                    --방어기능 적용하기 ( 추가되는 코드 )
                    
                    --방어하기를 누르면 해당 캐릭 Humanoid 안에 "blockAction" 이라는 값이 생성됨 (BlockingSystem2참고)
                    local blockAction = enemyHumanoid:FindFirstChild("blockAction")
                    
                    -- blockAction값이 있으면 방어중
                    if blockAction then
                        print("상대방 방어 중")

                        Hitbox:Destroy()	-- Hitbox제거 (제거시키지 않으면 Touched 이벤트가 여러번 발생. 잘 모르겠으면 주석처리 후 실행 해볼 것)
                        HitEffect(enemyHumrp)	-- 나중에 BlockEffect() 함수로 변경 예정
                        Knockback(enemyHumrp)
                        PlayBlockSound(Player)
                        
                    -- blockAction값이 없으면 타격성공
                    else
                         --[[
                    1. 타격시 몸의 방향을 타격을 가한 상대방쪽으로
                    2. 타격 이펙트 생성
                    3 타격하자마자 Hitbox 제거(안해주면 연속 타격됨
                    4. 데이미 입히고
                    5. 맞은 플레이어 맞는 애니메이션 실행
                    6. 맞는 플레이어 넉백 실행
                    7. 타격 소리 플레이 
                    
                    추가 : 타격 당할 시 못움직이게 캐릭터 속도, 점프력 0으로 세팅 될 예정
                    ]]--



                        -- 타격시, 몸의 방향을 타격을 가한 상대방으로 향하게 한다. 
                        -- Humrp.Orientation.Y(상대방의 Y축 방향 값) + 180도 = 상대방과 마주보는 방향
                        local targetDegree = Humrp.Orientation.Y + 180	

                        -- 때리는 캐릭터의 Y축을 기준으로 180도 만큼 맞는 캐릭터를 회전시킨다. == 맞는 캐릭터가 때리는 캐릭터와 마주보게 됨
                        enemyHumrp.Orientation = Vector3.new(0,targetDegree,0)	


                        --타격 이펙트 생성
                        HitEffect(enemyHumrp)

                        --Hitbox 제거
                        Hitbox:Destroy()

                        --데미지 입히기 (Humanoid 내장함수임)
                        --현재는 Damage변수가 이 스크립트 내에 있지만 후에는 변수저장하는 모듈스크립트로 빼기
                        enemyHumanoid:TakeDamage(Damage)	


                        --타격시 맞는 애니메이션 실행
                        -- 맞는 액션을 실행시키기 위해 상대방의 Animator를 가져온다.
                        --이부분 추후 질문
                        -- 상대 휴머노이드 개체에 Animator가 기본으로 없다. 그럼 언제 Animator을 달고 그 안에 맞는 애니메이션들을 넣어놓지?
                        --warn("콤보값: "..combo)
                        local enemyAnimator = enemyHumanoid:FindFirstChildOfClass("Animator")
                        --warn("enemyAnimator: "..enemyAnimator)
                        local react = enemyAnimator:LoadAnimation(eAnims[combo])
                        --warn("react: "..react)
                        react:Play()



                        Knockback(enemyHumrp)	-- 넉백 실행			
                        PlayHitSound(Player)	-- 타격 소리 플레이
                    
                    end

                end
            end
               
        end
        
    end)
    
end)
    
    
    

 

 

 

카메라 고정 전/ 공격하면서 이동이 안되는 것을 볼 수 있다.

 

다음에는 기본 애니메이션 제거 ( 상대방을 climb 하는 애니메이션 등) , 공격 콤보 추가, 공격시 이펙트 커지기 등을 다룰 예정이다.

댓글