반응형

파티클은 아니지만 차일드 윈도우로 화면에 내리는 눈을 만들어 보자.

 

// 차일드 윈도우가 100개 정도만 되어도 엄청 느려지고 잘 안된다.
// 그러다 그 이상되면 101번째 윈도우 생성시(정확히는 렌더러 생성시) 아래와 같은 Access Violation이 발생한다.
// Exception thrown at 0x00007FF8EE239D68 (nvd3dumx.dll) in SDLTest.exe: 0xC0000005 : Access violation reading location 0x0000000000000038.
// SDL3가 문제인지, 그래픽 드라이버가 문제인지... 정확히는 모르겠지만 아무래도 그래픽 드라이버 문제같다.

#define SDL_MAIN_USE_CALLBACKS 1
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <stdio.h>
#include <windows.h>

SDL_Window* window = NULL;
SDL_Renderer* renderer = NULL;

const int number_flakes = 10;
int screen_width;
int screen_height;

SDL_Window* subwindow[number_flakes]; // 서브 윈도우
SDL_Renderer* subrenderer[number_flakes]; // 서브 윈도우 렌더러

typedef struct position {
	float x, y;
	float vx, vy;
} s_position;
s_position subwindow_pos[number_flakes]; // 서브 윈도우 위치

// 파티클 이미지 텍스쳐와 렉트
SDL_Texture* texture[number_flakes];
SDL_FRect rect;

SDL_AppResult SDL_AppInit(void** appstate, int argc, char* argv[])
{
	ShowWindow(GetConsoleWindow(), SW_HIDE);
	// 콘솔 창 숨김. Project - Property Pages - Linker - System - SubSystem - Windows로 바꿔서 컴파일해도 된다.

	SDL_SetAppMetadata("Example", "1.0", "sean");

	if (!SDL_Init(SDL_INIT_VIDEO)) {
		SDL_Log("Couldn't initialize SDL: %s", SDL_GetError());
		return SDL_APP_FAILURE;
	}

	// 부모	윈도우 생성하고 최소화
	if (!SDL_CreateWindowAndRenderer("Press ESC to Quit", 320, 32, SDL_WINDOW_MINIMIZED, &window, &renderer)) {
		SDL_Log("Couldn't create window/renderer: %s", SDL_GetError());
		return SDL_APP_FAILURE;
	}

	// 비트맵 로드
	SDL_Surface* bmpSurface = SDL_LoadBMP("snow.bmp");
	if (!bmpSurface) {
		SDL_Log("Couldn't load BMP: %s", SDL_GetError());
		return SDL_APP_FAILURE;
	}

	// 컬러키(투명) 설정
	SDL_SetSurfaceColorKey(bmpSurface, true, SDL_MapSurfaceRGB(bmpSurface, 0x00, 0x00, 0x00));
	//SDL_SetSurfaceColorKey(bmpSurface, true, SDL_MapRGB(SDL_GetPixelFormatDetails(bmpSurface->format), NULL, 0x00, 0x00, 0x00));
	rect = { 0, 0, (float)bmpSurface->w, (float)bmpSurface->h };

	// 현재 디스플레이 정보 가져오기(해상도, 주사율 등)
	const SDL_DisplayMode* displaymode = SDL_GetCurrentDisplayMode(SDL_GetPrimaryDisplay());
	screen_width = displaymode->w / 3;
	screen_height = displaymode->h / 3;

	for (int i = 0; i < number_flakes; i++) {
		subwindow_pos[i].x = (float)SDL_rand(screen_width);
		subwindow_pos[i].y = (float)SDL_rand(screen_height); // 서브 윈도우 위치 설정
		subwindow_pos[i].vx = SDL_randf() * 0.5f;
		subwindow_pos[i].vy = SDL_randf() + 0.8f;

		/*
		// 서브 윈도우 생성
		SDL_PropertiesID props = SDL_CreateProperties(); // Create Properties
		SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_TRANSPARENT_BOOLEAN, true);
		SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_BORDERLESS_BOOLEAN, true);
		SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_ALWAYS_ON_TOP_BOOLEAN, true);
		SDL_GetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_MENU_BOOLEAN, true);
		SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_WIDTH_NUMBER, rect.w);
		SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_HEIGHT_NUMBER, rect.h);
		SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_X_NUMBER, subwindow_pos[i].x);
		SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_Y_NUMBER, subwindow_pos[i].y);
		SDL_SetPointerProperty(props, SDL_PROP_WINDOW_CREATE_PARENT_POINTER, window);

		subwindow[i] = SDL_CreateWindowWithProperties(props);
		if (!subwindow[i]) {
			SDL_Log("Couldn't create subwindow: %s", SDL_GetError());
			return SDL_APP_FAILURE;
		}

		subrenderer[i] = SDL_CreateRenderer(subwindow[i], NULL);
		if (!subrenderer[i]) {
			SDL_Log("Couldn't create subwindow renderer: %s", SDL_GetError());
			return SDL_APP_FAILURE;
		}
		*/

		// 위 주석의 코드와 동일
		if (!SDL_CreateWindowAndRenderer("Sub Window", (int)rect.w, (int)rect.h, SDL_WINDOW_BORDERLESS | SDL_WINDOW_ALWAYS_ON_TOP | SDL_WINDOW_TRANSPARENT, &subwindow[i], &subrenderer[i])) {
			SDL_Log("Couldn't create subwindow and subrenderer: %s", SDL_GetError());
			return SDL_APP_FAILURE;
		}
		SDL_SetWindowParent(subwindow[i], window); // 서브 윈도우 부모 설정		
		SDL_SetWindowPosition(subwindow[i], (int)subwindow_pos[i].x, (int)subwindow_pos[i].y);

		SDL_SetRenderDrawBlendMode(subrenderer[i], SDL_BLENDMODE_BLEND);
	}

	for (int i = 0; i < number_flakes; i++) {
		texture[i] = SDL_CreateTextureFromSurface(subrenderer[i], bmpSurface);
		if (!texture[i]) {
			SDL_Log("Couldn't create texture: %s", SDL_GetError());
			return SDL_APP_FAILURE;
		}
	}

	SDL_DestroySurface(bmpSurface);

	return SDL_APP_CONTINUE;
}

SDL_AppResult SDL_AppEvent(void* appstate, SDL_Event* event)
{
	switch (event->type) {
	case SDL_EVENT_QUIT:
		return SDL_APP_SUCCESS;
	case SDL_EVENT_KEY_DOWN:
		printf("Key pressed: %s\n", SDL_GetKeyName(event->key.key));
		if (event->key.key == SDLK_ESCAPE)
			return SDL_APP_SUCCESS;
		break;
	default:
		break;
	}

	return SDL_APP_CONTINUE;
}

SDL_AppResult SDL_AppIterate(void* appstate)
{
	SDL_Delay(1000 / 60); // 60 FPS

	SDL_SetRenderDrawColor(renderer, 0, 0, 0, SDL_ALPHA_OPAQUE);
	SDL_RenderClear(renderer);

	SDL_RenderPresent(renderer);

	for (int i = 0; i < number_flakes; i++) {
		SDL_SetRenderDrawColor(subrenderer[i], 0, 0, 0, SDL_ALPHA_TRANSPARENT);
		SDL_RenderClear(subrenderer[i]);
		SDL_RenderTexture(subrenderer[i], texture[i], NULL, &rect);
		SDL_RenderPresent(subrenderer[i]);

		// 서브 윈도우 위치 업데이트
		subwindow_pos[i].x += subwindow_pos[i].vx; // 오른쪽으로 이동
		subwindow_pos[i].y += subwindow_pos[i].vy; // 아래로 이동

		if (subwindow_pos[i].x > screen_width) {
			subwindow_pos[i].x = 0; // 화면 왼쪽으로 다시 이동
		}
		if (subwindow_pos[i].y > screen_height) {
			subwindow_pos[i].y = 0; // 화면 위로 다시 이동
		}
		SDL_SetWindowPosition(subwindow[i], (int)subwindow_pos[i].x, (int)subwindow_pos[i].y);
	}

	return SDL_APP_CONTINUE;
}

void SDL_AppQuit(void* appstate, SDL_AppResult result)
{
	for (int i = 0; i < number_flakes; i++) {
		SDL_DestroyTexture(texture[i]);
		SDL_DestroyRenderer(subrenderer[i]);
		SDL_DestroyWindow(subwindow[i]);
	}

	SDL_DestroyRenderer(renderer);
	SDL_DestroyWindow(window);

	SDL_Quit();
}

 

 

 

하지만 위 예처럼 눈의 수 만큼 차일드 윈도우를 생성하는건 부담스럽다.

특별한 비활성화 투명 윈도우를 생성하는 방식으로 바꿔보자.

 

#define SDL_MAIN_USE_CALLBACKS 1
#include "SDL3/SDL.h"
#include "SDL3/SDL_main.h"
#include <stdio.h>

#include <windows.h>
#include <WinUser.h>

SDL_Window* window = NULL;
SDL_Renderer* renderer = NULL;

int screen_width = GetSystemMetrics(SM_CXSCREEN);
int screen_height = GetSystemMetrics(SM_CYSCREEN);

Uint64 start;
Uint64 end;
float fps;

typedef struct {
	float x, y;
	float vx, vy;
} s_particle;

const int number_particles = 2000;
s_particle particles[number_particles];

// 파티클 이미지 텍스쳐와 렉트
SDL_Texture* texture;
SDL_FRect rect;

void spawn_particle(s_particle* pt) {
	pt->x = (SDL_randf() * 2 - 1) * screen_width * 10;
	pt->y = SDL_randf() * screen_height * -2;
	// 넓은 범위에서 눈이 생성되게 해서 실행 초기에 눈이 쏟아져 내리는걸 방지.
	pt->vx = SDL_randf() * 0.5f; // 눈은 오른쪽으로만 흩날린다.
	pt->vy = SDL_randf() + 0.8f;
}

void draw_particle(s_particle pt[]) {
	for (int i = 0; i < number_particles; i++) {
		s_particle* particle = &pt[i];
		particle->x += particle->vx;
		particle->y += particle->vy;
		// 눈이 화면 왼쪽에서도 바람에 날려 올 수 있도록 생존 범위 조정.
		if (particle->x > screen_width || particle->x < -screen_width || particle->y > screen_height)
			spawn_particle(particle);

		// 텍스쳐 그리기
		rect.x = particle->x - rect.w / 2;
		rect.y = particle->y - rect.h / 2;
		SDL_RenderTexture(renderer, texture, NULL, &rect);

		//SDL_SetRenderDrawColor(renderer, 255, 0, 0, SDL_ALPHA_OPAQUE);
		//SDL_RenderPoint(renderer, particle->x, particle->y);
	}
}

SDL_AppResult SDL_AppInit(void** appstate, int argc, char* argv[])
{
	SDL_SetAppMetadata("Example", "1.0", "sean");

	if (!SDL_Init(SDL_INIT_VIDEO)) {
		SDL_Log("Couldn't initialize SDL: %s", SDL_GetError());
		return SDL_APP_FAILURE;
	}

	SDL_SetHint(SDL_HINT_RENDER_VSYNC, "1");
	// A variable controlling whether updates to the SDL screen surface should be synchronized
	// with the vertical refresh, to avoid tearing.

	// 디스플레이 스케일 정보 확인
	SDL_DisplayID display = SDL_GetPrimaryDisplay(); // Return the primary display
	SDL_Log("DisplayID: %u", display);
	float display_scale = SDL_GetDisplayContentScale(display);
	SDL_Log("Display Scale: %f", display_scale);
	SDL_Log("Original Display Size: (%d X %d)", screen_width, screen_height);

	// 스케일 적용
	screen_width = int(screen_width * display_scale);
	screen_height = int(screen_height * display_scale);

	SDL_Log("Scaled Display Size: (%d X %d)", screen_width, screen_height);

	if (!SDL_CreateWindowAndRenderer("example", screen_width, screen_height, SDL_WINDOW_TRANSPARENT | SDL_WINDOW_BORDERLESS | SDL_WINDOW_FULLSCREEN, &window, &renderer)) {
		SDL_Log("Couldn't create window/renderer: %s", SDL_GetError());
		return SDL_APP_FAILURE;
	}

	// 투명 배경 설정을 위해 블렌드 모드 설정
	SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);

	// 윈도우 핸들 가져오기
	SDL_PropertiesID props = SDL_GetWindowProperties(window);
	HWND hwnd = (HWND)SDL_GetPointerProperty(props, SDL_PROP_WINDOW_WIN32_HWND_POINTER, NULL);
	// 윈도우 투명 및 클릭 무시 설정
	SetWindowLongPtr(hwnd, GWL_EXSTYLE, GetWindowLongPtr(hwnd, GWL_EXSTYLE) | WS_EX_LAYERED | WS_EX_TRANSPARENT);
	// 항상 위에 표시, 사이즈, 위치 변경, 활성화 금지 
	SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);

	for (int i = 0; i < number_particles; i++) {
		spawn_particle(&particles[i]);
	}

	// 비트맵 로드
	SDL_Surface* bmpSurface = SDL_LoadBMP("snow.bmp");
	if (!bmpSurface) {
		SDL_Log("Couldn't load BMP: %s", SDL_GetError());
		return SDL_APP_FAILURE;
	}

	// 컬러키(투명) 설정
	SDL_SetSurfaceColorKey(bmpSurface, true, SDL_MapSurfaceRGB(bmpSurface, 0x00, 0x00, 0x00));
	//SDL_SetSurfaceColorKey(bmpSurface, true, SDL_MapRGB(SDL_GetPixelFormatDetails(bmpSurface->format), NULL, 0x00, 0x00, 0x00));
	rect = { 0, 0, (float)bmpSurface->w, (float)bmpSurface->h };
	texture = SDL_CreateTextureFromSurface(renderer, bmpSurface);

	SDL_DestroySurface(bmpSurface);

	return SDL_APP_CONTINUE;
}

SDL_AppResult SDL_AppEvent(void* appstate, SDL_Event* event)
{
	switch (event->type) {
	case SDL_EVENT_QUIT:
		return SDL_APP_SUCCESS;
	case SDL_EVENT_KEY_DOWN:
		printf("Key pressed: %s\n", SDL_GetKeyName(event->key.key));
		if (event->key.key == SDLK_ESCAPE)
			return SDL_APP_SUCCESS;
		break;
	default:
		break;
	}

	return SDL_APP_CONTINUE;
}

SDL_AppResult SDL_AppIterate(void* appstate)
{
	// FPS 계산. 사용하지는 않음.
	start = SDL_GetPerformanceCounter();

	{
		// 반투명 배경으로 클리어
		SDL_SetRenderDrawColor(renderer, 0, 0, 0, 128);
		SDL_RenderClear(renderer);

		draw_particle(particles);

		SDL_RenderPresent(renderer);
	}

	end = SDL_GetPerformanceCounter();
	fps = 1.0f / ((end - start) / (float)SDL_GetPerformanceFrequency());

	return SDL_APP_CONTINUE;
}

void SDL_AppQuit(void* appstate, SDL_AppResult result)
{
	SDL_DestroyTexture(texture);

	SDL_DestroyRenderer(renderer);
	SDL_DestroyWindow(window);
	SDL_Quit();
}

 

※ 참고

위 코드에서는 화면 배율이 1이 아닌 경우를 위해 screen_width, screen_height 등의 정보를 Windows API를 이용해 확인하고, 화면 배율(display_scale)을 적용해 정확한 스크린 사이즈를 결정한다. 화면 배율이 1이 아닌 경우 정확한 스크린 사이즈 확인은 아래 링크와 같이 하는게 더 낫다.

 

2025.06.02 - [SDL, raylib] - [SDL3] Display Information 디스플레이 정보 가져오기

 

살짝 어두운 배경으로 바뀌고 눈이 내린다. 동시에 다른 프로그램을 사용할 수 있다.

 

 

※ 참고

2025.04.18 - [SDL, raylib] - [SDL3] Particle 파티클 (Fireworks/Snow)

 

반응형
Posted by J-sean
:
반응형

부모 윈도우와 자식 윈도우의 입력을 구분해 처리해 보자.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
#define SDL_MAIN_USE_CALLBACKS 1
 
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <stdio.h>
#include <Windows.h>
 
static SDL_Window* window = NULL;
static SDL_Renderer* renderer = NULL;
 
static SDL_Window* childWindow = NULL;
static SDL_Renderer* childRenderer = NULL;
 
SDL_AppResult SDL_AppInit(void** appstate, int argc, char* argv[])
{
    SDL_SetAppMetadata("Example""1.0""Sean");
 
    // Initialize SDL with video subsystem
    if (!SDL_Init(SDL_INIT_VIDEO)) {
        SDL_Log("Couldn't initialize SDL: %s", SDL_GetError());
        return SDL_APP_FAILURE;
    }
 
    // 메인 윈도우 및 렌더러 생성
    if (!SDL_CreateWindowAndRenderer("Parent Window"6404800&window, &renderer)) {
        SDL_Log("Couldn't create window/renderer: %s", SDL_GetError());
        return SDL_APP_FAILURE;
    }
 
    printf("Main window ID: %d\n", SDL_GetWindowID(window));
 
    // 자식 윈도우 및 렌더러 생성
    if (!SDL_CreateWindowAndRenderer("Child Window"6404800&childWindow, &childRenderer)) {
        SDL_Log("Couldn't create child window/renderer: %s", SDL_GetError());
        return SDL_APP_FAILURE;
    }
 
    printf("Child window ID: %d\n", SDL_GetWindowID(childWindow));
 
    // 메인 윈도우를 자식 윈도우의 부모 윈도우로 설정
    if (!SDL_SetWindowParent(childWindow, window)) {
        SDL_Log("Couldn't set child window parent: %s", SDL_GetError());
        return SDL_APP_FAILURE;
    }
 
    // 윈도우 핸들 구하기
    SDL_PropertiesID prop = SDL_GetWindowProperties(window);
    HWND hwnd = (HWND)SDL_GetPointerProperty(prop, SDL_PROP_WINDOW_WIN32_HWND_POINTER, NULL);
    if (hwnd)
        printf("Main window handle: %p\n", hwnd);
 
    prop = SDL_GetWindowProperties(childWindow);
    HWND hpwnd = (HWND)SDL_GetPointerProperty(prop, SDL_PROP_WINDOW_WIN32_HWND_POINTER, NULL);
    if (hpwnd)
        printf("Popup window handle: %p\n", hpwnd);
 
    return SDL_APP_CONTINUE;
}
 
SDL_AppResult SDL_AppEvent(void* appstate, SDL_Event* event)
{
    switch (event->type) {
    case SDL_EVENT_QUIT:
        return SDL_APP_SUCCESS;
    case SDL_EVENT_KEY_DOWN:
        printf("Key pressed window: %d, Key: %s\n", event->key.windowID, SDL_GetKeyName(event->key.key));
        if (event->key.key == SDLK_ESCAPE)
            if (event->key.windowID == SDL_GetWindowID(window))
                return SDL_APP_SUCCESS; // Exit on Escape key in main window
            else if (event->key.windowID == SDL_GetWindowID(childWindow)) {
                // 자식 윈도우에서 Escape 키를 누르면 자식 윈도우 닫음
                SDL_DestroyRenderer(childRenderer);
                childRenderer = NULL;
                SDL_DestroyWindow(childWindow);
                childWindow = NULL;
            }
        break;
    case SDL_EVENT_MOUSE_BUTTON_DOWN:
        printf("Mouse button pressed window: %d, Button index: %d\n", event->button.windowID, event->button.button);
        printf("Mouse position: (%d, %d)\n", (int)event->button.x, (int)event->button.y);
        break;
    default:
        break;
    }
 
    return SDL_APP_CONTINUE;
}
 
SDL_AppResult SDL_AppIterate(void* appstate)
{
    SDL_SetRenderDrawColor(renderer, 255255255, SDL_ALPHA_OPAQUE);
    SDL_RenderClear(renderer);
    SDL_RenderPresent(renderer);
 
    if (childRenderer)
    {
        SDL_SetRenderDrawColor(childRenderer, 00255, SDL_ALPHA_OPAQUE);
        SDL_RenderClear(childRenderer);
        SDL_RenderPresent(childRenderer);
    }
 
    return SDL_APP_CONTINUE;
}
 
void SDL_AppQuit(void* appstate, SDL_AppResult result)
{
    if (childRenderer)
        SDL_DestroyRenderer(childRenderer);
    if (childWindow)
        SDL_DestroyWindow(childWindow);
 
    SDL_DestroyRenderer(renderer);
    SDL_DestroyWindow(window);
 
    SDL_Quit();
}
 

 

자식 윈도우에서 ESC키를 누르면 자식 윈도우만 종료된다.

 

반응형
Posted by J-sean
:
반응형

Instantiate로 생성한 씬을 반복 재사용해 보자.

 

스프라이트 하나를 생성하고 씬으로 저장(Player.tscn)한다.

 

res:// 에 스크립트를 생성하고 Autoload에 추가한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
using Godot;
 
public partial class Control : Node
{
    public PackedScene Scene;
    public Timer timer;
    public Sprite2D Sprite;
 
    public override void _Ready()
    {
        // C# has no preload, so you have to always use ResourceLoader.Load<PackedScene>().
        Scene = ResourceLoader.Load<PackedScene>("res://Player.tscn");
        Sprite = Scene.Instantiate<Sprite2D>();
 
        timer = new Timer();
        timer.Connect("timeout", Callable.From(OnTimeOut));
        timer.WaitTime = 1.0;
        AddChild(timer);
        timer.Start();        
    }
    
    public override void _Process(double delta)
    {        
    }
 
    public void OnTimeOut()
    {
 
        Sprite.Position = new Vector2(GD.RandRange(0, (int)GetViewport().GetVisibleRect().Size.X),
            GD.RandRange(0, (int)GetViewport().GetVisibleRect().Size.Y));
        if (Sprite.GetParent() == null )
        {
            AddChild(Sprite);
            // Adds a child node. Nodes can have any number of children, but every child must have
            // a unique name. Child nodes are automatically deleted when the parent node is deleted,
            // so an entire scene can be removed by deleting its topmost node.
        }
        else
        {
            RemoveChild(Sprite);
            // Removes a child node. The node is NOT deleted and must be deleted manually.
        }
    }
}
 

 

 

RemoveChild()로 제거한 자식 노드는 삭제되지는 않는다. AddChild()로 다시 추가 할 수 있다.

 

게임을 실행하면 씬에 스프라이트가 반복해서 나타났다 사라진다.

 

게임이 실행된 상태에서 Remote 탭을 확인하면 GlobalControl의 자식 노드로 Sprite2D가 반복적으로 추가됐다 삭제 된다.

 

반응형
Posted by J-sean
: