웹 설명서를 그대로 번역하면 적 신으로 나오는데, 마음에 들지않아 장애물이라고 표현하겠습니다. 장애물에 해당하는 몹(Mob)들은 화면 가장자리 4군데에서 랜덤하게 생성되고, 직선방향으로 움직이도록 할 계획입니다. 우선 Player에서 했던 것과 마찬가지로 새 신(scene)을 추가하고 Rigidbody2D 노드를 추가합니다. 그리고 이름은 Mob으로 바꿔주겠습니다.
그리고 아래의 버튼(잠금열쇠 옆에있는거..)을 클릭하여, 추가되는 자식노드만 따로따로 선택되지 않도록 해 줍니다.
이어서, Gravity Scale을 0으로 해 줍니다. 즉, 중력에 영향을 받지 않게 해줍니다. 이 값이 0이 아닐경우, Mob들은 중력의 영향을 받도록 계산되고, 화면 아래로 떨어지게됩니다. 조금 아래로 내려와 CollisionObject2D 카테고리의 Mask에 1을 체크 해제합니다. 이렇게 하면 Mob들끼리 서로 충돌하는 상황은 발생하지 않습니다.
2. 애니메이션 추가
Player 씬에서 수행했던 것과 마찬가지로, AnimatedSprite를 Node 하위에 추가하고,
인스펙터에서 Frames > 새 SpriteFrame 을 추가하여 한번 더 클릭하여 Animation 창을 띄워줍니다.
애니메이션은 fly, swim, walk의 세가지가 있으며, 각각의 케이스에 대해 2개의 이미지가 있습니다. 기존에 했던 것과 동일한 방식으로 이미지를 추가해줍니다. 아울러 재생속도를 초당 3프레임으로 변경해줍니다.
또한 Mob들의 사이즈를 조절하기 위해 AnimatedSprite 노드를 한번 더 클릭하고, 인스펙터 창의 Playing 속성을 "사용", Scale속성을 x, y 방향으로 각각 0.75씩 입력해줍니다.
3. 충돌 노드(CollisionShape2D) 추가
Player 씬과 마찬가지로 충돌 감지노드를 추가해야합니다. Mob 노드를 선택한 상태에서 새 노드를 추가해줍니다.
이번엔, CapsuleShape이면서도 Mob의 형상에 맞게 90도 회전하여 잘 커버되도록 조정해줍니다.
4. Mob.gd 스크립트
스크립트에서는 Mob이 생성될때 랜덤 형상, 랜덤 속도, 화면 밖으로 나갔을 때 삭제 처리를 할 예정입니다.
extends RigidBody2D
export var min_speed = 150 # Minimum speed range.
export var max_speed = 250 # Maximum speed range.
# Called when the node enters the scene tree for the first time.
func _ready():
var mob_types = $AnimatedSprite.frames.get_animation_names()
$AnimatedSprite.animation = mob_types[randi() % mob_types.size()]
func _on_VisibilityNotifier2D_screen_exited():
queue_free()
# Called every frame. 'delta' is the elapsed time since the previous frame.
#func _process(delta):
# pass
mob_types는 애니메이션에서 설정한 3가지의 몹 이름을 배열로 갖게 됩니다. 그리고 실제 보여줄 에니메이션에서는 이 중 랜덤하게 골라서 설정하게 됩니다.
화면에서 해당 객체(생성된 Mob)가 나가게 되면 자체 제거(메모리 회수)를 하게되는데, 이때 사용하는 함수가 queue_free()입니다.
프로젝트 > 프로젝트 설정을 클릭한 후, display > window메뉴로 가서 width480, height720으로 설정해줍니다.
그리고, Stretch로 내려가서 Mode: 2d, Aspect: keep으로 변경해줍니다.
2. Resource 폴더 추가
dodge_assets 파일은 링크 에서 다운받을 수 있습니다. Godot엔진 홈페이지의 Your First Game 챕터에서 찾을 수도 있습니다. 압축을 풀어서 생성된 프로젝트 폴더에 집어넣으면 아래와 같이 자동으로 프로젝트에 폴더가 추가됩니다.
3. Player 씬 추가
왼쪽 씬 탐색기에서 다른노드 > Area2D검색 >Area2D 를 선택합니다.
노드를 더블클릭하여 이름을 Player로 바꿔줍니다. 그리고 Player.tscn으로 저장합니다.
그리고 자물쇠 아이콘 옆의 아이콘을 클릭해줍니다. 추후 자식 노드들이 추가될텐데, 자식노드들만 따로 선택되는 것을 방지해주는 기능이라고 합니다.
Player 노드 하위에 AnimatedSprite를 검색하여 추가합니다.
AnimatedSprite노드가 추가되면 우측의 Frames속성이 아직 [비었음] 상태입니다. [비었음]을 클릭하여 새 SpriteFrames를 선택합니다.
그리고 선택된 SpriteFrame를 한번 더 눌러주면 아래의 애니메이션 패널이 나타납니다.
4. 애니메이션 프레임 추가
Default를 walk라고 이름을 바꾸고, 새 애니메이션 추가 버튼을 클릭하여 up으로 이름으로 만들어줍니다. 아까 추가한 dodge_assets 폴더로 가보면 art라는 하위폴더가 있는데, 여기에서 PlayerGrey_walk1, 2는 walk프레임에, PlayerGrey_up1, 2는 up프레임에 드래그앤드랍으로 추가해줍니다.
다시 AnimationSprite노드를 클릭하고, 인스펙터 창에서 Node2D > Scale에서 x와 y를 각각 0.5로 입력하여 사이즈를 반으로 줄여줍니다.
5. 충돌감지노드 추가
Player노드에서 우클릭하여 새 노드를 추가합니다.검색창에 CollisionShape2D를 검색하여 추가해줍니다.
이 노드는 현재의 노드가 장애물(Mob)과 충돌을 감지할 수 있는 영역을 만들수 있게 해 줍니다. 즉, 이 노드만으로는 안되고, 실제 감지되는 영역을 지정해줘야합니다. 인스펙터 창에서 Shape > [비었음] 클릭 > 새 CapsuleShape2D 를 클릭하여 추가해줍니다.
그러면 캐릭터에 캡슐형 감지영역이 나타나게 됩니다. 감지영역의 핸들을 조정하여 적당히 커버될 수 있도록 조정합니다. 약간 이미지보다 작게 나타나도록 설정하는 것을 추천합니다.
6. Player 방향키 설정
적당히 진행하며 저장해주시고, 이제 Player의 움직임을 설정해줘야합니다. 여기서부터 코딩이 들어갑니다. Attach Script 버튼을 클릭합니다.
우선 필요한 변수를 선언합니다. (speed, screen_size)
export 키워드는 작업을 굉장히 편리하게 해 줍니다. 변수 앞에 export를 붙여주면 inpector 창에서 나타나게 됩니다. 그럼 inpector에서 값을 직접 설정하여, 테스트할 때 일일이 코드를 보지 않고도 테스트할 수 있도록 도와줍니다. 물론 스크립트에 설정된 기본값은 무시됩니다.
처음 게임이 시작되면 _ready()함수를 실행하게 되는데, 게임 창의 크기값을 가져와 screen_size라는 변수에 지정하도록 하겠습니다. 이 값은 나중에 활용할 계획입니다. 기존에 있던 pass 부분은 지워줍니다. 아무것도 안하는 함수일 때 필요한 지시어입니다.
func _process(delta):
var velocity = Vector2() # The player's movement vector.
if Input.is_action_pressed("ui_right"):
velocity.x += 1
if Input.is_action_pressed("ui_left"):
velocity.x -= 1
if Input.is_action_pressed("ui_down"):
velocity.y += 1
if Input.is_action_pressed("ui_up"):
velocity.y -= 1
if velocity.length() > 0:
velocity = velocity.normalized() * speed
$AnimatedSprite.play()
else:
$AnimatedSprite.stop()
우선 $AnimatedSprite.play() 의$는 get_node()의 단축 표기형입니다. get_node("AnimatedSprite").play()와 동일합니다. 위 코드는 아직 오류가 있는 것이 상하키와 좌우키를 동시에 누르면 1+1=2의 속도로 원래 움직임보다 더 빠르게 움직입니다. 다음으로 화면 밖으로 나가는 것을 방지하기 위해 clamp()함수로 영역을 지정해줍니다. func _process(delta)함수 하부에 이어서 아래 코드를 추가해줍니다.
func start(pos):
position = pos
show()
$CollisionShape2D.disabled = false
<전체 코드:Player.gd>
extends Area2D
signal hit
export var speed = 400
var screen_size
# Called when the node enters the scene tree for the first time.
func _ready():
screen_size = get_viewport_rect().size
hide()
# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta):
var velocity = Vector2() # The player's movement vector.
if Input.is_action_pressed("ui_right"):
velocity.x += 1
if Input.is_action_pressed("ui_left"):
velocity.x -= 1
if Input.is_action_pressed("ui_down"):
velocity.y += 1
if Input.is_action_pressed("ui_up"):
velocity.y -= 1
if velocity.length() > 0:
velocity = velocity.normalized() * speed
$AnimatedSprite.play()
else:
$AnimatedSprite.stop()
# position
position += velocity * delta
position.x = clamp(position.x, 0, screen_size.x)
position.y = clamp(position.y, 0, screen_size.y)
# 방향 역전
if velocity.x != 0:
$AnimatedSprite.animation = "walk"
$AnimatedSprite.flip_v = false
$AnimatedSprite.flip_h = velocity.x < 0
elif velocity.y != 0:
$AnimatedSprite.animation = "up"
$AnimatedSprite.flip_v = velocity.y > 0
func start(pos):
position = pos
show()
$CollisionShape2D.disabled = false
func _on_Player_body_entered(body):
hide() # Player disappears after being hit.
emit_signal("hit")
$CollisionShape2D.set_deferred("disabled", true)
그리고, 위젯에 요일이 제대로 표시가 안되고 네모로 나타나고 있습니다. 이건, Conky에서 설정한 폰트가 시스템에 없어서 그런건데요. .conky/ 폴더로 들어가면 fonts라는 폴더가 있습니다. 그곳에 있는 폰트 파일들을 /usr/share/fonts로 복사해줍니다. 이상한 것은 해당 위젯의 폰트가 GE_inspera.ttf인데, .conky/Gotham/Gotham파일에 보면 폰트가 GE Inspira로 되어있습니다.(언더바 빠짐). 이부분을 수정하여 GE_Inspira로 바꿔줍니다. 그러면 정상적으로 나옵니다.
1:N관계에서 N쪽 테이블(TempDataroomHstry클래스)에 ForeignKey로 1쪽 테이블명(Temp래스)을 지정해줍니다. 이때, 1쪽 테이블의 참조하려는 필드가 Primary Key로 지정되어있으면 상관없지만, 없을 경우 필드 정의에(ForeignKey 함수 내부에서) to__field='목표필드' 를 지정해줘야 합니다. 그리고, db_column='참조칼럼명' 에서 해당 테이블에서 참조할 실제 칼럼(필드)명을 지정해줘야 합니다.
(*) Oracle DB에서만 이런 문제가 발생하는 것인지.. 아직은 잘 모르겠습니다. 이것 때문에 한참을 헤메었네요..^^;; 또한 이번의 경우 Temp의 emp_field와 TempDataroomHstry의 id는 사실 칼럼명은 동일하게 'emp_#'이었습니다. db_column에 넣어주는 값이 현재 테이블의 칼럼명인지, 목표 테이블의 칼럼명인지 좀 헷갈립니다...
<models.py>
from django.db import models
class Temp(models.Model):
emp_field = models.CharField(db_column='emp_#', primary_key=True, max_length=7) # Field renamed to remove unsuitable characters. Field renamed because it ended with '_'.
emp_x = models.CharField(max_length=2, blank=True, null=True)
kornm_n = models.CharField(max_length=32, blank=True, null=True)
res_1 = models.CharField(db_column='res_#1', max_length=12, blank=True, null=True) # Field renamed to remove unsuitable characters.
sex_n = models.CharField(max_length=2, blank=True, null=True)
dept_c = models.CharField(max_length=16, blank=True, null=True)
class Meta:
managed = False
db_table = 'TEMP'
class TempDataroomHstry(models.Model):
seq_field = models.IntegerField(db_column='seq_#',primary_key=True) # Field renamed to remove unsuitable characters. Field renamed because it ended with '_'.
id = models.ForeignKey(Temp, to_field='emp_field', db_column='emp_#',on_delete=models.CASCADE, null=True, related_name='id') #, related_name='tempdataroomhstry'
in_d = models.DateField(blank=True, null=True)
class Meta:
managed = False
db_table = 'TEMP_DATAROOM_HSTRY'
2. 데이터 활용하기
Join을 위해서는 "select_related()"나 "prefetch_related()"를 사용하는데, 이번에는 select_related()만 알아보도록 하겠습니다. select_related는 1:1 또는 1:N 의 경우에 사용할 수 있는 함수입니다. (정방향 참조필드). select_related()의 인자로는 해당 Table의 ForeinKey를 넣어줍니다. 아래의 예제에서는 우선 'id'칼럼을 이용하여 join후 모든 데이터를 불러오고 'in_d'를 기준으로 역정렬(desc)을 하여 list를 만들고, 이를 home.html에 넘겨줍니다.
<views.py>
from django.core.paginator import Paginator
from django.http import HttpResponseRedirect
from django.shortcuts import render
from django.urls import reverse
from .models import TempDataroomHstry
#### 초기화면 및 조회함수 ####
def index(request):
list = {}
list = TempDataroomHstry.objects.select_related('id').all().order_by('-in_d')
# HTML에서 인자가 전달될 경우 처리
q = request.GET.get('q', '')
if q:
list = list.filter(id=q)
# 여기까지
context = {'member_list': list}
return render(request, 'home.html', context)
A테이블에 B가 조인될 경우 "[A].[조인된A칼럼명].[B칼럼명]"과 같이 사용하면 됩니다. 아래 home.html파일은 실제 Join된 데이터를 불러와 사용하는 예시를 볼 수 있습니다.
<home.html>
....
<tbody>
{% if member_list %}
{% for member in member_list %}
<tr>
<td>{{ member.id.emp_field }}</td>
<td>{{ member.id.kornm_n }}</td>
<td>{{ member.id.res_1 }}</td>
<td>{{ member.id.sex_n }}</td>
<td>{{ member.id.dept_c }}</td>
<td>{{ member.in_d }}</td>
</tr>
{% endfor %}
{% endif %}
</tbody>
이번에는 Webview 라이브러리를 사용하면서 HTML 파일을 별도로 분리하는 방법을 알아보겠습니다. 그리고, 좀더 보기좋은 GUI 구성을 위해 부트스트랩도 함께 적용하도록 하겠습니다. 사실 이부분은...CSS 파일을 별도로 인식시켰으면 좋겠지만, 아직까지 별도의 CSS파일을 적용시키는 것은 안되는 것 같습니다.
main.go 파일에 전체 프로그램 구동을 구현합니다. 아울러, 버튼을 클릭했을 때 동작할 함수(go_hello)를 작성하고, 바인딩해줍니다. Webview라이브러리를 활용하면, go에서 javascript를 실행할 수도 있고, html파일(javascript 포함)에서 go언어를 실행할 수도 있습니다. 그러나 프로그램 가독성을 위해, Bind 한줄이 더 들어가더라도, 함수구현은 go에서 하도록 하겠습니다.
package main
import (
"fmt"
"os"
"github.com/webview/webview"
)
var w webview.WebView
func main() {
w = webview.New(true)
defer w.Destroy()
w.SetSize(600, 600, webview.HintNone) // Create a GoLang function callable from JS
w.Bind("go_hello", go_hello) // Go_hello 함수 구현과 html에서 호출하는 go_hello를 Bind해줍니다.
// Create UI with data URI
dir, _ := os.Getwd()
fmt.Println(dir)
w.Navigate("file:" + dir + "/hi.html")
w.Run()
}
// 함수 실행 시, javascript로 팝업 알람을 실행해서 데이터를 보여줍니다.
// HTML문서에서 보여주는 부분만큼은...javascript로..ㅠㅠ
func go_hello() {
name := "You"
w.Eval(`alert("Hello` + name + `");`)
}
데이터를 처리하는 것은 go 함수구현에서 할 수 있지만, 마지막으로 데이터를 HTML로 보내서 보여주는 부분은 Javascript로 할 수밖에 없겠네요..^^;;
3. hi.html
HTML로 뷰를 구현합니다. 서두에서 말했듯이 CSS 적용이 힘든 관계로 Bootstrap을 CDN으로 불러와 적용시키겠습니다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-giJF6kkoqNQ00vy+HMDP7azOuL0xtbfIcaT9wjKHr8RbDVddVHyTfAAsrekwKmP1" crossorigin="anonymous">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.16.0/umd/popper.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
<title>My App</title>
</head>
<body>
<!--Header Section-->
<div class="box-padding-big">
<nav class="navbar navbar-expand-sm navbar-dark bg-dark">
<div class="collapse navbar-collapse">
<ul class="navbar-nav mr-auto">
<li class="nav-item active">
<a class="nav-link" href="#" id=navbardrop" data-toggle="dropdown">File</a>
<div class="dropdown-menu">
<a class="dropdown-item" href="#" onclick="go_hello()">Sub1</a>
<a class="dropdown-item" href="#">Sub2</a>
<a class="dropdown-item" href="#">Sub3</a>
</div>
</li>
<li class="hav-item">
<a class="nav-link" href="#">Edit</a>
</li>
<li class="hav-item">
<a class="nav-link" href="#">View</a>
</li>
<li class="hav-item">
<a class="nav-link" href="#">Info</a>
</li>
</ul>
</div>
</nav>
</div>
<!--Main Section-->
<div style="margin: 20px;">
<div style="font-size: 36px; color:chartreuse;font-weight:700">
<div>GO-Webview HTML View Test <br/>
with Bootstrap
</div>
</div>
<p class="text-primary" style="margin-top:20px;">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam eget magna eros. Cras sit amet nulla dignissim, pretium justo sit amet, vulputate orci. Curabitur in aliquam lorem. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Morbi quis pulvinar nunc, sed posuere enim. In hac habitasse platea dictumst. Ut sodales augue eu elit bibendum, ut malesuada felis auctor. Sed mattis ipsum malesuada sem feugiat ultricies. Fusce ultrices vel est id pretium. Sed vel congue augue, non tincidunt tellus. Vivamus facilisis mollis tellus ac vulputate.
</p>
</div>
</body>
</html>
4. 결과
아래 명령으로 결과를 확인합니다.
go run main.go
File - Sub1을 클릭하면...
알람 팝업이 잘 나옵니다.
이상으로 webview를 이용하여 html 파일과 bootstrap을 적용하는 방법을 알아봤습니다. 너무 쉽게 구현이 가능한데, 성능은 어느정도일지, 어떤 점이 부족할지 계속 알아봐야겠습니다.