반응형

1. routes 수정

/movie/#123456과 같이 ID를 입력받을 수 있도록 수정

import { createRouter, createWebHashHistory} from 'vue-router'
import Home from './MyHome.vue'
import Movie from './MyMovie.vue'
import About from './MyAbout.vue'

export default createRouter({
  // Hash, History --> Hash mode 사용 예정
  history: createWebHashHistory(),
  // pages
  routes:[
    {
      path:'/',
      component: Home
    },
    {
      path:'/movie/:id',
      component: Movie
    },
    {
      path:'/about',
      component: About
    }

  ]
})

2. ./store/movie.js :단일 영화 상세정보 가져오기

import axios from 'axios';
import _uniqBy from 'lodash/uniqBy'

export default {
  // module화 부분
  namespaced:true,
  // 실제 데이터
  state: () => {
    return {
      movies:[],
      message:'Search for the movie title!',
      loading: false,
      theMovie: {

      }
    }
  },
  // computed와 동일
  getters:{
    movieIds(state){
      return state.movies.map(m => m.imdbID)
    }
  },
  // methods와 동일
  // 변이: state 데이터 변경
  mutations:{
    updateState(state, payload){
      // ['movies','message','loading']
      Object.keys(payload).forEach(key => {
        // state.movies = payload.movies
        // state.message = payload.message
        // state.loading = payload.loading
        state[key] = payload[key]
      })
    },
    resetMovies(state){
      state.movies =[]
    }
  },
  // 기본적으로 비동기 처리
  actions: {
    async searchMovies(context, payload){
      // Search 실행중이면 함수 종료
      if (context.state.loading) return
      
      // message 초기화
      context.commit('updateState', {
        message: '',
        loading: true
      });
      try {
        const res = await _fetchMovie({
          ...payload,
          page: 1,
        });
        const { Search, totalResults } = res.data
        console.log(totalResults)
        context.commit('updateState', {
          movies: _uniqBy(Search, 'imdbID')
        })
        console.log(totalResults)
        console.log(typeof totalResults)
  
        const total = parseInt(totalResults, 10) //문자열 totalResults를 10진수 정수로 변경
        const pageLength = Math.ceil(total / 10) //한페이지에 10개씩
  
        if(pageLength > 1) {
          for(let page = 2; page <= pageLength; page+=1){
            if(page > payload.number / 10) break
            const res = await _fetchMovie({
              ...payload,
              page
            });
            const { Search } = res.data
            context.commit('updateState',{
              movies: [
                ...context.state.movies, 
                ..._uniqBy(Search, 'imdbID')
              ]
            })
          }
        }
      } catch (message){
        context.commit("updateState", {
          movie: [],
          message,
        });
      } finally {
        context.commit('updateState', {
          loading: false,
        })
      }
    },
    async searchMovieWithId(context, payload){     
      if (context.state.loading) return
      
      context.commit('updateState', {
        theMovie: {}, //기존의 검색결과를 초기화
        loading: true
      })

      try{
        const res = await _fetchMovie(payload)
        context.commit('updateState', {
          theMovie:res.data
        })
      } catch (error){
        context.commit('updateState', {
          theMovie:{}
      })
      } finally {
        context.commit('updateState', {
          loading: false
        })
      }
    }
  }
}
function _fetchMovie(payload){
  const { title, type, year, page, id } = payload;
  const OMDB_API_KEY='abd6b67a'
  const url = id 
      ? `https://www.omdbapi.com/?apikey=${OMDB_API_KEY}&i=${id}`
      :`https://www.omdbapi.com/?apikey=${OMDB_API_KEY}&s=${title}&type=${type}&y=${year}&page=${page}`;

  return new Promise((resolve, reject) => {
    axios.get(url)
      .then((res)=> {
        if (res.data.Error){ // 아무 검색어 없이 엔터입력 시 에러처리
          reject(res.data.Error);
        }
        resolve(res);
      })
      .catch((err)=> {
        reject(err.message);
      });
  });
}

3. ./components/MyLoader.vue

비동기 요청시 로더 별도로 분리

<template>
  <div 
    :class="{ absolute,fixed }" 
    class="spinner-border"
    :style="{
      width: `${size}rem`,
      height: `${size}rem`,
      zIndex
    }"> 
    <svg role="status" class="inline w-8 h-8 mr-2 text-gray-200 animate-spin dark:text-gray-600 fill-yellow-400" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg">
      <path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="currentColor"/>
      <path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentFill"/>
    </svg>
  </div>
</template>  

<script>
export default {
  props: {
    size: {
      type: Number,
      default: 2
    },
    absolute: {
      type: Boolean,
      default: false
    },
    fixed: {
      type: Boolean,
      default: false
    },
    zIndex: {
      type: Number,
      default: 0
    }
  }
}
</script>

<style scoped>
.spinner-border {
  margin: auto;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;  
}
.absolute {
  position: absolute;
}
.fixed {
  position: fixed;
}
</style>

3. ./routes/MyMovie.vue

Loading시 보여줄 스켈레톤 UI와 Loading후 보여줄 상세페이지

<template>
  <div class="container">
    <template v-if="loading">
      <div class="skeletons">
        <div class="skeleton poster"></div>
        <div class="specs">
          <div class="skeleton title"></div>
          <div class="skeleton spec"></div>
          <div class="skeleton plot"></div>
          <div class="skeleton etc"></div>
          <div class="skeleton etc"></div>
          <div class="skeleton etc"></div>          
        </div>      
      </div>
      <Loader 
        :size="3"
        :z-index="9"
        fixed />
    </template>
    <div v-else class="movie-details">
      <div 
        :style="{ backgroundImage: `url(${theMovie.Poster})` }"
        class="poster"></div>
      <div class="specs">
        <div class="title">
          {{ theMovie.Title }}
        </div>
        <div class="labels">
          <span>{{ theMovie.Released }}</span>
          <span>{{ theMovie.Runtime }}</span>
          <span>{{ theMovie.Country }}</span>
        </div>
        <div class="plot">
          {{ theMovie.Plot }}
        </div>
        <div class="ratings">
          <h3>Ratings</h3>
        </div>
        <div>
          <h3>Actors</h3>
          {{ theMovie.Actors}}
        </div>
        <div>
          <h3>Director</h3>
          {{ theMovie.Director }}
        </div>
        <div>
          <h3>Production</h3>
          {{ theMovie.Production }}
        </div>
        <div>
          <h3>Genre</h3>
          {{ theMovie.Genre }}
        </div>
      </div>
    </div>

  </div>
</template>

<script>
import Loader from '../components/MyLoader.vue'
export default {
  components: {
    Loader
  },
  computed: {
    theMovie() {
      return this.$store.state.movie.theMovie;
    },
    loading() {
      return this.$store.state.movie.loading
    }
  },
  created() {
    this.$store.dispatch('movie/searchMovieWithId',{
      id:this.$route.params.id
    })
  }
}
</script>

<style scoped>
.container {
  padding-top: 40px;
  margin: 0 auto;
}
.skeletons{
  display: flex;
}
.skeletons .poster {
  flex-shrink: 0;
  width: 500px;
  height: 750px;
  margin-right: 70px;
}
.skeletons .specs {
  flex-grow: 1;
}
.skeletons .skeleton {
  border-radius:  10px;
  background-color: lightgray;
}
.skeletons .title {
  width: 80%;
  height: 70px;
}
.skeletons .spec {
  width: 60%;
  height: 30px;
  margin-top: 20px;
}
.skeletons .plot {
  width: 100%;
  height: 250px;
  margin-top: 20px;
}
.skeletons .etc {
  width: 50%;
  height: 50px;
  margin-top: 20px;
}
.movie-details{
  display: flex;
  color: gray;
}
.movie-details .poster {
  flex-shrink: 0;
  width: 500px;
  height: 750px;
  margin-right: 70px;
  border-radius: 10px;
  background-color: lightgray;
  background-size: cover;
  background-position: center;
}
.movie-details .specs {
  flex-grow: 1;
}
.movie-details .specs .title {
  color: black;
  font-family: 'Oswald', sans-serif;
  font-size: 70px;
  line-height: 1;
  margin-bottom: 30px;
}
.movie-details .specs .labels {
  color: rgb(138, 138, 23);
}
.movie-details .specs .labels span::after{
  content:"\00b7";
  margin: 0 6px;
}
.movie-details .specs .labels span:last-child::after{
  display: none;
}
.movie-details .specs .plot {
  margin-top: 20px;
}
.movie-details .specs .ratings {}
.movie-details .specs h3 {
  margin: 24px 0 6px;
  color: black;
  font-family: "Oswald", sans-serif;
  font-size: 20px;
}
</style>
반응형

'Programming > Vue' 카테고리의 다른 글

Vue3 + bootstrap5 적용하기  (0) 2022.05.28
Vuex 사용  (0) 2022.05.24
Vue 영화검색사이트 기본 설치 파일  (0) 2022.05.14
Vue Tailwind CSS 적용하기  (0) 2022.05.13
Vue apache 배포 오류  (0) 2022.05.12
반응형

1.라이브러리 설치

npm i vue-router vuex bootstrap@5
npm i -D node-sass sass-loader

 

2. bootstrap 커스터마이징

- scss/main.scss 작성

- 오류 방지를 위해 maps는 주석 처리

// Required
@import "../../node_modules/bootstrap/scss/functions";

// Default variable overrides
$primary:#FDC000; //variables가 실행되기 전에 재정의되어야 함

// Required
@import "../../node_modules/bootstrap/scss/variables";
@import "../../node_modules/bootstrap/scss/mixins";
@import "../../node_modules/bootstrap/scss/root";
// @import "../../node_modules/bootstrap/scss/maps";

@import "../../node_modules/bootstrap/scss/bootstrap.scss";

 

3. Bootstrap navigation 버튼의 Pill 활성화(active) 예시

- components/Header.vue

<template>
  <header>
    <div class="nav nav-pills">
      <div v-for="nav in navigations"
        :key="nav.name"
        class="nav-item">
        <RouterLink 
          :to="nav.href"
          active-class="active"  
          class="nav-link">
          <!-- bootstrap의 nav Pill속성의 Active 클래스인 "active"로 지정 -->
          {{ nav.name }}
        </RouterLink>
      </div>
    </div>
  </header>
</template>

<script>
export default{
  data() {
    return {
      navigations:[
        {
          name:'Search',
          href:'/'
        },
        {
          name:'Movie',
          href:'/movie'
        },
        {
          name:'About',
          href:'/about'
        }
      ]
    }
  }
}
</script>
반응형
반응형

1. 설치

npm install -D tailwindcss@latest postcss@latest autoprefixer@latest

2. 활성화

npx tailwindcss init -p

 

3. ./tailwind.config.js환경설정

module.exports = {
  // content: [
  //   "./index.html",
  //   "./src/**/*.{vue,js,ts,jsx,tsx}",
  // ],
  purge:[
    "./index.html",
    "./src/**/*.{vue,js,ts,jsx,tsx}"
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}

 

4. src/index.css 파일 생성 

@import "tailwindcss/base"; 
@import "tailwindcss/components"; 
@import "tailwindcss/utilities";

 

5. src/main.js적용

import { createApp } from 'vue'
import App from './App.vue'
import './index.css'

createApp(App)
.mount('#app')
반응형
반응형

Vue로 작성한 프로그램을 Build를 하고...

npm run build

 

/dist/ 폴더의 파일들을 아파치 폴더에 옮겼는데, 빈 화면이 나옵니다. index.html 파일은 보이는데 개발자 화면 확인해보니...js파일과 css파일의 경로를 인식을 못했습니다.

"/js/chunk-vendors....."처럼 되어있는데, 혹시나 해서 "."을 붙여보니 작동이 됩니다. 

동일한 오류 겪고계신 분께서는 한번 시도해보시길 바랍니다.

반응형
반응형

Dotnet 프로그램의 Icon을 변경하는 것은 굉장히 쉽습니다. 원하는 아이콘(111.ico)이미지를 프로젝트 폴더에 넣어준 뒤, project.csproj파일에 등록시켜주면 끝납니다.

<PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>netcoreapp3.1</TargetFramework>
	<ApplicationIcon>111.ico</ApplicationIcon>
    <UseWPF>true</UseWPF>
</PropertyGroup>

반응형
반응형

Path2D노드 사용법에 대해 알아보겠습니다. 

Path2D노드를 사용하면 단순히 움직이는 캐릭터들(Enemy 나 NPC 등)의 단순 이동동작을 최소한의 코딩으로 구현할 수 있는 유용한 노드입니다.

 

1. 적당한 Enemy 노드가 있을 경우, 전체 동작하는 Scene에서 아래와 같이 구성을 하였습니다. Path2D노드는 하위에 항상 PathFollow2D를 자식으로 담고 있어야 합니다. Path2D노드가 포인트를 이용하여 라인을 생성하는 노드라면, PathFollow2D노드는 그 선을 어떤 방식으로 따라가는지를 설정하는 노드입니다.

2. Path2D노드를 다시 선택하면 아래 그림과 같이 에디터 상단에 포인터 추가/삭제/편집할 수 있는 버튼이 활성화됩니다. 적당히 이동 경로를 그려줍니다. 이 중 5번째 아이콘은 경로를 닫아주는 버튼입니다. 라인을 완전히 그리지 말고, 적당히 그린 후 이 버튼을 누르면 라인을 닫으면서 닫힌 도형으로 만들어줍니다.

3. PathFollow2D노드를 선택한 후 Rotate를 체크 해제합니다. (경우에 따라 다릅니다만, 저는 라인을 따라 회전하지 않고, 좌우 평행이동만 하려고 합니다.

4. Enemy.gd스크립트 내부에 아래의 라인을 추가해줍니다. (_process(delta)가 없다면, _physics_process(delta)함수에 추가해주시기 바랍니다.)

# 변수로 선언하여 PathFollow2D를 불러옴
onready var path_follow = get_parent()
var speed = 20 # 움직이는 속도

func _process(delta):	
	path_follow.set_offset(path_follow.get_offset() + speed *delta)
반응형
반응형

오늘은 AnimationTree노드에 대해 알아보겠습니다.

 

처음 캐릭터의 애니메이션을 만들면 AnimationPlayer노드를 많이 사용하는데요, 이 노드만 이용해서 각종 상태를 코드로 컨트롤하려면 좀 힘듭니다. 많이 힘듭니다. 머리는 쥐가나고, 코드는 스파게티가 되고, 뭐 하나라도 추가하려면 완전...

그래서 여기서도 똑똑하신분이 상태 관리하라고 노드를 하나 주셨는데, 그게 바로 AnimationTree노드입니다. AnimationTree노드를 사용하려면 각각의 동작 Animation은 사전에 구현이 되어있어야합니다. 이미 존재하는 Animation을 컨트롤하는 상태관리자라고 보시면 됩니다. 참고로 저는 코로나 격리기간 중에 아래와 같은 게임을 7살 딸과 함께 만드는 중이었습니다. 탑다운 방식이다보니 상하좌우방면의 idle/walk(run)/attack의 애니메이션이 모두 필요한 상태입니다.

1. AnimationTree노드 추가

우선은 해당 노드를 한번 추가해줍니다. 그리고 인스펙터에서 Tree Root에서 "새 AnimationNodeStateMachine"을 선택합니다. 다른 강좌 영상을 봐도 거의 이거 하나만 씁니다. 아직 아래의 작업공간에는 아무것도 나타나지 않습니다. (제 화면의 idle박스와 walk박스는 미리 작업해 놓은 것이므로 무시하세요.)

내부의 분홍색 라인은 카메라 때문에 나타납니다. 무시하세요.

2. Add BlendSpace2D

화면 하단의 AnimationTree작업공간에서 빈 공간에 마우스 우클릭 후 Add BlendSpace2D를 선택/추가해줍니다. 그리고 이름을 idle이라고 바꾸도록 하겠습니다. 동일한 방법으로 하나 더 추가하여 walk도 만듭니다.

3. Animation 노드 연결

메뉴에서 화살표를 선택하고 idle박스에서 walk박스로 드래그해주면 그 방향으로 화살표 선이 나타나 연결됩니다. 동일한 방법으로 반대쪽도 수행해줍니다.

다음으로 idle박스를 선택한 후 빨간색으로 표시된 버튼을 눌러서, idle이 게임 시작의 최초 "시작"상태임을 인식시켜줍니다. 

(ps) 각 화살표를 클릭하면 인스펙터에서 설정 가능한 부분들이 나타납니다. 여기서 다룰 부분이 아니므로, 이번에는 패스하도록 하겠습니다.  

 

4. BlendSpace편집

idle박스의 편집버튼을 누르면 BlendSpace편집창이 나타납니다.

이미지상에 하단부분 클릭도 빼놓지 않고 추가해줍니다.

X축 및 Y축이 -1 ~ +1까지 있는 좌표계가 나타납니다. 이는 마우스, 키보드 등의 입력값을 말합니다. 즉 캐릭터의 현재 위치를 (0, 0)으로 보고, 이를 중심으로 마우스가 어디에서 눌렸는지를 받아들여 이에 대한 반응을 보여줍니다. (키보드일 경우는 중간이 없고 상/하/좌/우만 있겠죠?). 주의할 점은 Y값의 1이 화면에서는 아래쪽을 의미합니다. 즉, 위 그림에서 보이는 것처럼 (0, 1) 지점에 idle_down 애니메이션을 지정해아 합니다.

메뉴 아이콘의 3번째 버튼이 포인트 추가버튼입니다. 선택 후 위의 그림과 같이 마름모꼴의 각 지점을 클릭하면 에니메이션 추가를 할 수 있습니다. 각각의 좌표계에 애니메이션을 아래와 같이 추가합니다. 또한 혼합 버튼을 ...으로 변경합니다.

 

(-1, 0) : idle_left

(+1, 0): idle_right

(0, -1) : idle_top

(0, +1): idle_down

 

여기까지 진행하면 BlendSpace2D지정은 완료가 됩니다.

 

5. 테스트

이번엔 잘 지정이 되었는지 테스트를 해보겠습니다. 메뉴의 위쪽에 경로: root를 선택합니다. 

그럼 애니메이션 박스를 만들었던 화면으로 전환되는데, 여기서 idle버튼을 플레이합니다.(박스 안쪽에 작은 실행버튼을 클릭합니다.) 그리고 다시 편집 버튼을 눌러 아까의 BlendSpace편집 화면으로 들어갑니다.

그리고 마커 버튼을 선택한 다음 마름모 영역 안에서 마우스를 드래그 하면 캐릭터가 해당 방향으로 애니메이션을 전환하는 모습을 볼 수 있습니다.

동일한 방법으로 walk 애니메이션도 추가해 줍니다. (attack애니메이션도 있으면 추가합니다.)

 

6. 스크립트 코딩

그럼 이렇게 만든 애니메이션을 어떻게 사용하는지 보겠습니다. 우리가 사용할 idle애니메이션은 "AnimationTree"노드의 "parameters/playback"이라는 속성에 존재합니다. 

이 속성을 활용하기 위해 anim_mode = anim_tree.get("parameters/playback")을 사용합니다. 그리고 실제 애니메이션을 구현하는 것은 anim_mode.travel 함수입니다. anim_mode.travel("idle")과 같이 사용하면 idle애니메이션을 실행합니다.

extends KinematicBody2D


const SPEED = 120
var direction = Vector2(0,0)
var motion = Vector2(0,0)
onready var anim_tree = $AnimationTree
onready var anim_mode = anim_tree.get("parameters/playback")


func _physics_process(delta):
	controls()
	movement()	
	anim_control()
	
func controls(): 
# 키보드 입력을 받아서 방향을 정하는 함수
	var LEFT = Input.is_action_pressed("ui_left")
	var RIGHT = Input.is_action_pressed("ui_right")
	var UP = Input.is_action_pressed("ui_up")
	var DOWN = Input.is_action_pressed("ui_down")
	
	direction.x = -int(LEFT) + int(RIGHT)
	direction.y = -int(UP) + int(DOWN)
	
	
func movement():
# 방향을 크기 1인 벡터 motion으로 만들고, 이를 이용해 동작을 구현
	motion = direction.normalized()
	move_and_slide(motion * SPEED, Vector2(0,0))

func anim_control():
# 방향에 따라 애니메이션 지정. 애니메이션의 방향 값만 설정해 놓음
	match direction:
		Vector2(-1,0):
			anim_tree.set('parameters/walk/blend_position', Vector2(-1,0))
			anim_tree.set('parameters/idle/blend_position', Vector2(-1,0))
		Vector2(1,0):
			anim_tree.set('parameters/walk/blend_position', Vector2(1,0))
			anim_tree.set('parameters/idle/blend_position', Vector2(1,0))
		Vector2(0,-1):
			anim_tree.set('parameters/walk/blend_position', Vector2(0,-1))
			anim_tree.set('parameters/idle/blend_position', Vector2(0,-1))	
		Vector2(0, 1):
			anim_tree.set('parameters/walk/blend_position', Vector2(0, 1))
			anim_tree.set('parameters/idle/blend_position', Vector2(0, 1))	
	
    # 방향이 설정된 상태에서 애니메이션을 선택
	if motion.length()!=0: # motion의 크기가 0이 아니면 이동중이므로 walk 애니
		anim_mode.travel("walk")
	else:		# motion의 크기가 0이면 멈춤이므로 idle 애니
		anim_mode.travel("idle")

 

이상으로 AnimationTree노드의 BlendSpace2D속성을 이용한 애니메이션 구현을 알아보았습니다. 

 

~~끝~~

반응형
반응형

이미지.zip
0.04MB


1. Display Size를 (400,500)으로 세팅합니다.
2. Borderless를 설정합니다.
3. Per Pixel Transparency를 Allowed, Enabled 로 바꿉니다.

4. 노드 구성
- clock (control)
ㄴ ColorRect (ColorRect)
ㄴ Body (sprite) : 시계 몸체 이미지
ㄴ pivot_h (Node2D)
ㄴ Hour (sprite) : 시침 이미지
ㄴ pivot_m (Node2D)
ㄴ Minute (sprite) : 분침 이미지
ㄴ pivot_s (Node2D)
ㄴ Second (sprite) : 초침 이미지
5. Body는 앱 중앙에 위치시키고, pivot들은 Body의 중앙에, pivot 내부의 sprite이미지들은 위와같이 정렬해줍니다. Godot 엔진에서 이미지를 특정 포인트를 기준으로 돌리기가 어렵습니다. 그래서 모두 가운데를 기준으로 돌리되, sprite이미지를 아예 적당한 거리에 떨어트려놓으면 가운데를 기준으로 돌아가는 효과를 만들 수 있습니다.

6. 스크립팅 (clock.gd)

extends Control var following = false 
var start_pos = Vector2() 

func _ready(): 
	get_tree().get_root().set_transparent_background(true) 
   
func _physics_process(delta): # ESC버튼으로 프로그램 종료 기능 
	if Input.is_action_pressed("ui_cancel"): 
    	get_tree().quit() # 시침/분침/초침의 회전 각도 계산 
    
    $Body/pivot_h.rotation = (OS.get_time()["hour"] + OS.get_time()["minute"] / 60.0) * PI/6 
    $Body/pivot_m.rotation = OS.get_time()["minute"] * PI/30 
    $Body/pivot_s.rotation = OS.get_time()["second"] * PI/30 
    
# 마우스 클릭&드래그로 시계 프로그램 위치 이동 
func _on_clock_gui_input(event): 
	if event is InputEventMouseButton: 
    	if event.get_button_index()==1: 
        	following = !following 
            start_pos = get_local_mouse_position() 
    if following: 
        OS.set_window_position(OS.window_position + get_local_mouse_position() - start_pos)


<결과>

반응형

+ Recent posts