본문 바로가기
Develop Story/Game Designer

Lua - 전투 시스템 만들기 예제

by 늘상의 하루 2021. 1. 25.

 

이번에는 루아를 활용하여 직접 예제를 하나 구상했습니다.

 

앞서 만든 가위바위보를 응용하여 간단한 전투 시스템을 만들 예정입니다.

메인 화면에서 게임을 시작한 다음 직업을 고르고 슬라임과 전투를 벌일 겁니다.

 

직업은 전사, 마법사 두 가지로 한정하고 적은 랜덤한 체력을 가진 슬라임을 출현시키겠습니다.

전투는 TRPG 스타일로 턴제 방식을 채용하고 양측 다 주사위를 굴려 미스, 명중, 크리티컬을 결정하겠습니다.

 

나름 스킬도 넣고 랜덤하게 대응하는 AI를 만들 예정입니다.

 

그리고 다른 언어에는 있지만 루아에는 없는 함수들이 존재하기 때문에 편의를 위해서 두 가지 함수를 만들어 활용할 예정입니다.

 

아래의 내용으로 진행하겠습니다.

 

1. 게임 시작

2. 직업 선택

3. 전투 구현


local clock = os.clock

function wait(n)
	local waitfortime = clock()
	while clock() -waitfortime <= n do end
end

function enterText(n)	
	for i =0, n do
		print("")
	end
end

 

진행하기에 앞서 두 함수를 만들었습니다. wait와 enterText입니다.

 

wait는 입력된 숫자(n)값 만큼 프로그램을 대기시키는 함수입니다. os.cloack 은 0부터 시간을 측정하는 함수이며 while 문에서 설정된 값(n)보다 커질 경우 루프를 종료하도록 작성했습니다.

 

enterText는 훨씬 단순합니다. 입력된 횟수만큼 엔터키를 누르는 것처럼 행을 넘겨 화면을 정리해주는 함수입니다. 

이제 간단하게 메인 화면부터 작성하도록 하겠습니다.

 

단순하지만 가시성을 높이기 위해 문자열을 활용하여 UI를 만들 계획입니다.

 

function MainMenu()
	local gamestartButton = 0
	enterText(20)
	print("   ┌──────────────────────────────┐")
	print("   │                              │")
	print("   │ 전투 시스템 테스트 준비 끝!  │")
	print("   │                              │")
	print("   └──────────────────────────────┘")	
	print("   ┌──────────────┬───────────────┐")
	print("   │▒▒▒▒▒▒▒▒▒▒▒▒▒▒│▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒│")
	print("   │ 1: 게임 시작 │  2: 게임 종료 │")
	print("   │▒▒▒▒▒▒▒▒▒▒▒▒▒▒│▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒│")
	print("   └──────────────┴───────────────┘")	
	gamestartButton = io.read("*n")
	
	if gamestartButton == 1 then
		choiceClass()
	elseif gamestartButton == 2 then
		print("3초 후 게임을 종료합니다.")
		wait(3)
	end
end

 

생각보다 단순한 구조입니다. 1을 입력하면 직업을 고르는 함수를 호출하고 2를 입력하면 그대로 종료합니다.

UI의 한글 같은 경우에는 보이는 것과 출력값의 위치가 다르게 나올 수 있으니 틈틈이 실행하면서 맞춰 주면 됩니다.

 

function choiceClass()
	math.randomseed(os.time())
	userClass = 0	
	enterText(20)
	print("   ┌──────────────────────────────┐")
	print("   │                              │")
	print("   │ 당신의 직업을 선택하세요!!!  │")
	print("   │                              │")
	print("   └──────────────────────────────┘")
	print("   ┌──────────────┬───────────────┐")
	print("   │▒▒▒▒▒▒▒▒▒▒▒▒▒▒│▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒│")
	print("   │ 1: 튼튼 전사 │  2: 쌘 마법사 │")
	print("   │▒▒▒▒▒▒▒▒▒▒▒▒▒▒│▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒│")
	print("   └──────────────┴───────────────┘")
	print("   ┌──────────────┬───────────────┐")
	print("   │▒▒▒▒▒▒▒▒▒▒▒▒▒▒│▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒│")
	print("   │   HP : 20    │   HP : 10     │")
	print("   │   MP : 1     │   MP : 10     │")
	print("   │   Dam : 4    │   Dam : 1     │")
	print("   │   MDam : 0   │   MDam : 7    │")
	print("   │▒▒▒▒▒▒▒▒▒▒▒▒▒▒│▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒│")
	print("   └──────────────┴───────────────┘")
	
	userClass = io.read("*n")
	
	if userClass == 1 then
		ClassWarrior()
	elseif userClass == 2 then
		ClassMage()
	end

	EnermySlime()
	WarSystem()
	
end

 

노가다인 print를 제외하면 아래의 내용 역시 단순합니다.

io.read로 입력값을 받고 입력값에 따라 1번 튼튼 전사를 고르거나 2번 쌘 마법사를 고르면 됩니다.

 

그리고 각자 맞는 함수를 호출하고 싸울 적 함수를 호출한 다음, 전투 시스템을 호출하면 됩니다.

 

function ClassWarrior()
	Name = "튼튼 전사"
	HP = 20
	MP = 1
	Dam = 4
	MDam = 0
end

function ClassMage()
	Name = "쌘 마법사"
	HP = 10
	MP = 10
	Dam = 1
	MDam = 7
end

function EnermySlime()
	print("   ┌──────────────────────────────┐")
	print("   │                              │")
	print("   │ 야생의 슬라임이 나타났다!!!  │")
	print("   │                              │")
	print("   └──────────────────────────────┘")
	
	EnermyName = "왕 슬라임"
	EnermyHP = math.floor(math.random(15,20))
	EnermyDam = 3
end

 

루아는 위에서 아래로 실행되기에 함수들은 호출되기에 앞서 먼저 정의되어야 합니다.

고로 직업 함수와 적 함수는 choiceClass()보다 앞에 작성되어야 합니다.

 

유저가 선택하는 직업의 이름, 체력, 마나, 데미지, 스킬데미지로 직업을 구분했고

적의 경우에는 단순하게 이름, 체력, 데미지만 남겼습니다.

 

슬라임의 체력 같은 경우에는 매번 도전할 때마다 바뀔 수 있도록 랜덤 값을 사용했습니다.

전투 시스템으로 넘어가겠습니다.

 

function StatUI()
	enterText(20)
	print("──────────────────────────────────────────")
	print("   적의 상태")
	print("   ┌──────────────┐")
	print("   │▒▒▒▒▒▒▒▒▒▒▒▒▒▒│")
	print("   │ 1: "..EnermyName.." │")
	print("   │ HP: "..EnermyHP.."       │")
	print("   │▒▒▒▒▒▒▒▒▒▒▒▒▒▒│")
	print("   └──────────────┘")
	print("──────────────────────────────────────────")
	print("──────────────────────────────────────────")
	print("   당신의 상태")
	print("   ┌──────────────┐───────────────┐")
	print("   │▒▒▒▒▒▒▒▒▒▒▒▒▒▒│▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒│")
	print("   │  "..Name.."   │   행동 목록   │")
	print("   │   HP : "..HP.."    │   공격 : 1    │")
	print("   │   MP : "..MP.."     │   방어 : 2    │")
	print("   │   Dam : "..Dam.."    │   스킬 : 3    │")
	print("   │   MDam : "..MDam.."   │   도주 : 4    │")
	print("   │▒▒▒▒▒▒▒▒▒▒▒▒▒▒│▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒│")
	print("   └──────────────┘───────────────┘")

end
function SkillList(n)	
	if n == 1 then
		print("  "..Name.."은 방어 태세에 들어갔다! ")
		wait(1)
		print("  "..Name.."은 어떤 공격도 막을 것 같다! ")
		wait(2)
	elseif n == 2 then
		print("  당신은 현란한 검놀림을 보여주었다!")
		wait(1)
		print("...")
		wait(1)
		print("  그건 아주 멋있었다! 슬라임도 감탄했다!")
		wait(1)
	elseif n == 3 then
		print("  당신은 파이어볼을 준비했다!")
		wait(1)
		print("  아주 뜨거운 불길이 만들어졌다!")
		wait(1)
		SkillDam = 1
        Attack()
	end
		
end
function WarSystem()
	enterText(20)
	while HP > 0 and EnermyHP > 0 do
		whatDo = 0
		WarDice1 = 0
		WarDice2 = 0
		DiceVal = 0

		StatUI()
		print("   행동을 입력하세요.")
		whatDo = io.read("*n")
		
		if whatDo == 1 then
			Attack()
		elseif whatDo == 2 then
			SkillList(1)	
		elseif whatDo == 3 then	
			if userClass==1 then
				SkillList(2)
			elseif userClass ==2 then
				SkillList(3)
			end
		elseif whatDo == 4 then
			print("   당신은... 도망쳤다!")
			wait(3)
			FinishGame()		
		end
		EnermyAIdo()
	end
	
	if HP == 0 then
		enterText(20)
		print("   ┌──────────────────────────────┐")
		print("   │                              │")
		print("   │ 멋진 "..EnermyName.."의 승리    !!!  │")
		print("   │                              │")
		print("   └──────────────────────────────┘")
		wait(1)
	elseif EnermyHP == 0 then
		enterText(20)
		print("   ┌──────────────────────────────┐")
		print("   │                              │")
		print("   │ "..Name.."의 승리      !!!  │")
		print("   │                              │")
		print("   └──────────────────────────────┘")
		wait(1)
	end
end

 

while문에서는 전투 시스템에서 굴릴 두 주사위의 변수값과 두 주사위를 합한 값을 루프마다 초기화 시켜주었습니다.

가시성을 위해 StatUI() 함수를 호출하는 것으로 난잡한 UI를 따로 작성했습니다.

 

내용을 보면 플레이어의 행동을 총 4가지로 규정[공격, 방어, 스킬, 도주]했음을 알 수 있습니다. 각 행동은 독립적으로 작성된 함수를 호출하도록 하였습니다.

 

공격은 Attack()

방어는 SkillList(1)

스킬은 SkillList(2), SkillList(3)

도주는 FinishGame()

 

스킬을 사용할 때는 플레이어의 직업을 확인하고 직업에 맞는 스킬이 나가도록 했습니다.

 

플레이어의 행동이 모두 끝나면 EnermyAIdo()를 통해 AI가 행동하도록 작성했습니다.

랜덤 함수를 활용하여 AI가 4가지 행동 중에서 하나를 선택해 움직이게 했습니다.

 

그리고 플레이어나 슬라임 체력이 0이 되면 루프가 끝나게 설정했고 아래의 조건문에서 누구의 승리인지 알 수 있도록 텍스트를 출력시켰습니다.

 

먼저 공격 함수를 보겠습니다.

 

function Attack()
	print("   당신의 공격! 과연 결과는? ")
	WarDice1 = math.floor(math.random(1,6))
	WarDice2 = math.floor(math.random(1,6))
	
	DiceVal = WarDice1 + WarDice2
	SkillDam = SkillDam*MDam
	
	wait(1)
	print(" 첫번째 주사위 : "..WarDice1.."")
	wait(1)
	print(" 두번째 주사위 : "..WarDice2.."")
	wait(1)
	print(" 그 결과는 ! : "..DiceVal.."")
	wait(1)
	
	if DiceVal < 5 then
		print(" 공격 미스! ")
	elseif 4 < DiceVal and DiceVal < 10 then
		print(" 공격 적중! ")
		if EnermyHP < (Dam+SkillDam) then
			EnermyHP = 0
		elseif EnermyHP > (Dam+SkillDam) then
			EnermyHP = EnermyHP-(Dam+SkillDam)
		end
	elseif 9 < DiceVal then
		print(" 크리티컬 데미지! ")
		if EnermyHP < (Dam+SkillDam) then
			EnermyHP = 0
		elseif EnermyHP > (Dam+SkillDam) then
			EnermyHP = EnermyHP-((Dam+SkillDam)*2)
		end
	end
	SkillDam = 0
	wait(1)
end

 

공격 함수에서는 math.floor(math.random())을 사용하여 두 개의 주사위를 각각 랜덤으로 굴린 다음 합한 값에서 결과를 도출할 수 있도록 구현했습니다.

-math.floor 를 쓰지 않으면 랜덤에서 난수를 소수점까지 발생시켜 결과값이 변하지 않는 것 같은 현상을 만듭니다. 꼭 해당 함수를 활용하여 정수 값만 뽑을 수 있도록 합시다.-

 

Diceval 변수에서 두 값을 합치고 조건문에서 주사위의 결과값에 따라 공격이 어떻게 됐는지 판정을 해 줍시다. 2~12 사이의 값이 있는데 제 경우에는 TRPG 처럼 2~4 값은 미스, 5~9 값은 성공, 10~12 값은 대성공으로 잡았습니다.

 

그리고 SkillDam에서 스킬 공격을 했을 때 스킬 데미지를 함께 연산할 수 있도록 간단한 수식을 적었습니다.

 

그리고 결과값에 따라 적의 체력 EnermyHP를 감소시켜 주시면 됩니다.

 

여기서 방금 만들어둔 wait 함수를 활용하겠습니다. 이걸 사용하지 않으면 유저가 인식하기 힘들 정도로 빠르게 텍스트가 출력되기 때문에 가시성을 위해 꼭 넣어줍시다.

 

WarSystem() 안에 다 쓰는게 아닌 이러한 방식으로 작성하면 차후 공격 방식을 수정하기에도 용이하고 직업을 추가하거나 변경할 때 편리하게 적용이 가능합니다.

 

마지막에는 꼭 SkillDam을 0으로 초기화시켜 주도록 합시다.

 

이제 슬라임의 공격을 구현해 보겠습니다. 상속을 쓰려고 했는데 루아에는 상속 기능이 없고 비슷하게 흉내를 낼 수 있다고 하여 그건 다음에 하고 이번에는 그냥 작성하도록 하겠습니다.

 

function EnermyAttack()
	wait(1)
	print("  "..EnermyName.."의 공격! 과연 그 결과는?")
	WarDice1 = math.floor(math.random(1,6))
	WarDice2 = math.floor(math.random(1,6))
	
	DiceVal = WarDice1 + WarDice2
	
	wait(1)
	print(" 첫번째 주사위 : "..WarDice1.."")
	wait(1)
	print(" 두번째 주사위 : "..WarDice2.."")
	wait(1)
	print(" 그 결과는 ! : "..DiceVal.."")
	wait(1)

	if DiceVal < 5 then
		print(" 공격 미스! ")
	elseif 4 < DiceVal and DiceVal < 10 then
		print(" 공격 적중! ")
		if HP < EnermyDam then
			HP = 0
		elseif HP > EnermyDam then
			HP = HP-EnermyDam
		end
	elseif 9 < DiceVal then
		print(" 크리티컬 데미지! ")
		if HP < EnermyDam then
			HP = 0
		elseif HP > EnermyDam then
			HP = HP-(EnermyDam*2)
		end
	end
	wait(1)
end

 

같은 방식을 사용하기 때문에 플레이어의 공격과 비슷합니다.

다음으로는 AI의 행동 패턴을 구성하는 함수를 작성하겠습니다.

 

function EnermyAIdo()

	AIdo = 0
	
	print("──────────────────────────────────────────")
	print("──────────────────────────────────────────")
	print("  "..EnermyName.."의 차례! 뭘 할까? ")
	print("──────────────────────────────────────────")
	print("──────────────────────────────────────────")
	
	AIdo = math.floor(math.random(1,4))

	if AIdo == 1 then
		print("  "..EnermyName.."은 공격하기로 마음먹었다!")
		
		if whatDo == 2 then
			print("  "..EnermyName.."의 공격은 실패했고 오히려 본인이 데미지를 받았다! ")
			EnermyHP = EnermyHP-2
			wait(1)
			print(" 적은 소량의 데미지를 받았다! ")
		else EnermyAttack() end
		
	elseif AIdo == 2 then
		print("  "..EnermyName.."은 아무것도 안 하기로 마음먹었다!")
		
	elseif AIdo == 3 then
		print("  "..EnermyName.."은 잠을 잔다!")
		wait(1)
		print("  "..EnermyName.."은 체력을 1 회복했다!")
		EnermyHP = EnermyHP+1
		
	elseif AIdo == 4 then
		print("  "..EnermyName.."은 공격으로 무방비해진 당신을 공격한다!")
		wait(1)
		
		if whatDo == 1 then
			print(" !!!!!!!!!!!!!!!!!!!!!!!")
			wait(1)
			print(" 공격은 성공적이었고 매우 치명적이다!")
			HP = HP-(EnermyDam*2)
			wait(1)
			print("  당신은 두 배의 데미지를 받았다!")
			wait(1)
		else print("  하지만 당신은 무방비하지 않았다!") end
		wait(1)
	end
	
	wait(3)
	
end

 

랜덤을 사용해서 4가지 동작 중 하나를 랜덤하게 선택하도록 했습니다.

 

1. 기본 공격 - 플레이어가 방어 상태일 때 역으로 피해를 받음.

2. 아무것도 안 하기

3. 잠자기 - 체력 1 회복

4. 기습 - 플레이어가 공격 상태일 때 두배의 데미지를 가함.

 

플레이어가 무엇을 하고 있는지 알 수 있는 whatDo 변수를 활용하여 조건문을 작성하고 대응하는 결과를 만들 수 있습니다. if문의 반복...노가다이기 때문에 내용을 뜯어보면 그렇게 어렵진 않습니다.

 

function FinishGame()
	print("계속하실? (그만하고 싶으면 10을 누르세요.)")
	StopNum = io.read("*n")
	if StopNum == 10 then
		Gamestop = 1
	end
end

Gamestop = 0
SkillDam = 0

while Gamestop do
	MainMenu()
	WarSystem()
	FinishGame()
	if Gamestop == 1 then
		break
	end
end

os.execute("pause")

이제 게임을 끝내는 FinishGame 함수와 게임을 계속 구동할 수 있도록 돌려주는 Gamestop 루프를 만들어 줍시다.

Gamestop과 SkillDam을 미리 선언해 주는것도 잊지 맙시다.

 

이걸로 전투 시스템 구현은 끝났습니다.

 

 

주말 간 루아를 공부하고 연습하는 겸 만들어 봤습니다. 하면서 텍겜을 만드는 것 같아 굉장히 재미있었습니다.

차후 좀 더 공부한 다음 응용하여 이와 비슷한 또 다른 예제를 만들어 보겠습니다. 

 

FightSystem.lua
0.01MB

다시 보니까 스킬을 썼을 때 마나가 감소하는걸 깜빡하고 안 만들었네요 ㅋㅋ