반응형

 

2023.10.14 - [Godot] - [Godot] 2D Splash Dynamic Wave 자연스러운 물결 파동 1. 준비

 

위 링크를 참고해 준비가 끝난 씬에 Polygon2D를 이용해 물 모양을 추가해 보자.

 

WaterBody에 Polygon2D를 추가한다.

 

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
using Godot;
using System.Collections.Generic;
 
public partial class WaterBody : Node2D
{
    public float k;
    public float d;
    public float spread;
    public int passes;
    public List<spring> springs;
    public int spring_number;
    public int distance_between_springs;
    public int depth;
    public int target_height;
    public int bottom;
    public PackedScene scene;
    public Polygon2D water_polygon;
 
    public override void _Ready()
    {
        k = 0.015f;
        d = 0.05f;
        spread = 0.0002f;
        passes = 8;
        depth = 1000;
        // 물의 깊이
        bottom = (int)GlobalPosition.Y + depth;
        // 물의 바닥 위치
        water_polygon = GetNode<Polygon2D>("Polygon2D");
        // Polygon2D 노드를 가져온다.
 
        springs = new List<spring>();
        spring_number = 10;
        distance_between_springs = 100;
        scene = ResourceLoader.Load<PackedScene>("res://spring.tscn");
        for (int i = 0; i < spring_number; i++)
        {            
            Node2D s = scene.Instantiate<Node2D>();            
            AddChild(s);            
            springs.Add(s as spring);
 
            int x_position = distance_between_springs * i + 100;
            (s as spring).Initialize(x_position);
        }
    }
 
    public override void _PhysicsProcess(double delta)
    {
        if (Input.IsActionJustPressed("ui_accept"))
            splash(520);
 
        foreach (spring item in springs)
        {
            item.water_update(k, d);
        }
 
        List<float> left_deltas = new List<float>();
        List<float> right_deltas = new List<float>();
 
        for(int i = 0; i < springs.Count; i++)
        {
            left_deltas.Add(0.0f);
            right_deltas.Add(0.0f);
        }
 
        for (int p = 0; p < passes; p++)
        {
            for (int i = 0; i < springs.Count; i++)
            {
                if (i > 0)
                {
                    left_deltas[i] = spread * (springs[i].height - springs[i-1].height);
                    springs[i-1].velocity += left_deltas[i];
                }
                if (i < springs.Count - 1)
                {
                    right_deltas[i] = spread * (springs[i].height - springs[i+1].height);
                    springs[i+1].velocity += right_deltas[i];
                }
            }
        }
 
        draw_waterbody();
        // 물 그리는 함수 호출.
    }
 
    public void splash(int index, float speed)
    {
        if(index >= 0 && index < springs.Count)
        {
            springs[index].velocity += speed;
        }
    }
 
    public void draw_waterbody()
    {
        List<Vector2> surface_points = new List<Vector2>();
        foreach (spring item in springs)
        {
            surface_points.Add(item.Position);
        }
        // 모든 스프링의 위치를 리스트에 추가한다.
 
        int first_index = 0;
        int last_index = surface_points.Count - 1;
 
        surface_points.Add(new Vector2(surface_points[last_index].X, bottom));
        surface_points.Add(new Vector2(surface_points[first_index].X, bottom));
        // 폴리곤을 사각형으로 만들기 위해 리스트 마지막에 사각형 밑변을 구성할 포인트를
        // 두 개 추가한다.
 
        water_polygon.Polygon = surface_points.ToArray();
        // 폴리곤을 구성하는 포인트 배열을 지정한다.
    }
}
 

 

 

WaterBody.cs 파일은 위와 같이 수정한다.

 

게임을 실행하고 엔터키를 누르면 폴리곤이 그려지고 스프링과 함께 물결처럼 운동한다.

 

좀 더 부드러운 물결을 만들어 보자.

 

WaterBody 씬에서 Polygon2D - Inspector - Visibility - Show Behind Parent 옵션을 체크한다.

 

 

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
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
using Godot;
using System.Collections.Generic;
 
public partial class WaterBody : Node2D
{
    public float k;
    public float d;
    public float spread;
    public int passes;
    public List<spring> springs;
    public int spring_number;
    public int distance_between_springs;
    public int depth;
    public int target_height;
    public int bottom;
    public PackedScene scene;
    public Polygon2D water_polygon;
    public Curve2D wave_position;
    // 부드러운 물결 곡선의 포인트를 저장할 Curve2D 변수
 
    public override void _Ready()
    {
        k = 0.015f;
        d = 0.05f;
        spread = 0.0002f;
        passes = 8;
        depth = 1000;
        bottom = (int)GlobalPosition.Y + depth;
        water_polygon = GetNode<Polygon2D>("Polygon2D");
 
        springs= new List<spring>();
        spring_number = 10;
        distance_between_springs = 100;
        scene = ResourceLoader.Load<PackedScene>("res://spring.tscn");
        for (int i = 0; i < spring_number; i++)
        {            
            Node2D s = scene.Instantiate<Node2D>();            
            AddChild(s);            
            springs.Add(s as spring);
 
            int x_position = distance_between_springs * i + 100;
            (s as spring).Initialize(x_position);
        }
 
        wave_position = new Curve2D();
        // 물결 곡선 Curve2D 초기화
    }
 
    public override void _PhysicsProcess(double delta)
    {
        if (Input.IsActionJustPressed("ui_accept"))
            splash(520);
 
        foreach (spring item in springs)
        {
            item.water_update(k, d);
        }
 
        List<float> left_deltas = new List<float>();
        List<float> right_deltas = new List<float>();
 
        for(int i = 0; i < springs.Count; i++)
        {
            left_deltas.Add(0.0f);
            right_deltas.Add(0.0f);
        }
 
        for (int p = 0; p < passes; p++)
        {
            for (int i = 0; i < springs.Count; i++)
            {
                if (i > 0)
                {
                    left_deltas[i] = spread * (springs[i].height - springs[i-1].height);
                    springs[i-1].velocity += left_deltas[i];
                }
                if (i < springs.Count - 1)
                {
                    right_deltas[i] = spread * (springs[i].height - springs[i+1].height);
                    springs[i+1].velocity += right_deltas[i];
                }
            }
        }
 
        draw_waterbody();
        draw_wave();
        // 부드러운 물결 그리기 함수 호출.
    }
 
    public void splash(int index, float speed)
    {
        if(index >= 0 && index < springs.Count)
        {
            springs[index].velocity += speed;
        }
    }
 
    public void draw_waterbody()
    {
        List<Vector2> surface_points = new List<Vector2>();
        foreach (spring item in springs)
        {
            surface_points.Add(item.Position);
        }
 
        int first_index = 0;
        int last_index = surface_points.Count - 1;
 
        surface_points.Add(new Vector2(surface_points[last_index].X, bottom));
        surface_points.Add(new Vector2(surface_points[first_index].X, bottom));
        
        water_polygon.Polygon = surface_points.ToArray();
    }
 
    public override void _Draw()
    {
        DrawPolyline(wave_position.Tessellate(), Colors.Blue, 5);
        // 물결 곡선을 그린다.
    }
 
    public void draw_wave()
    {
        wave_position.ClearPoints();
        // 매번 곡선의 포인트를 초기화 한다.
 
        Vector2 control0 = new Vector2(-500);
        Vector2 control1 = new Vector2(500);
        // 수평 컨트롤
 
        foreach (spring item in springs)
        {
            wave_position.AddPoint(item.Position, control0, control1);
            // 스프링 위치와 컨트롤을 곡선 포인트로 추가한다.
        }
 
        QueueRedraw();
        // _Draw() 호출
    }
}
 

 

 

WaterBody.cs 는 위와같이 변경한다.

 

spring 씬에서 Sprite2D - Inspector - Visibility - Visible 옵션을 해제한다.

 

Main 씬에서 실행한다.

 

폴리곤 위에 파란 물결이 부드러운 곡선으로 표시된다.

 

 

하지만 자세히 보면 폴리곤은 여전히 거칠다.

 

폴리곤을 부드럽게 바꿔보자.

 

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
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
using Godot;
using System.Collections.Generic;
 
public partial class WaterBody : Node2D
{
    public float k;
    public float d;
    public float spread;
    public int passes;
    public List<spring> springs;
    public int spring_number;
    public int distance_between_springs;
    public int depth;
    public int target_height;
    public int bottom;
    public PackedScene scene;
    public Polygon2D water_polygon;
    public Curve2D wave_position;
 
    public override void _Ready()
    {
        k = 0.015f;
        d = 0.05f;
        spread = 0.0002f;
        passes = 8;
        depth = 1000;
        bottom = (int)GlobalPosition.Y + depth;
        water_polygon = GetNode<Polygon2D>("Polygon2D");
 
        springs= new List<spring>();
        spring_number = 10;
        distance_between_springs = 100;
        scene = ResourceLoader.Load<PackedScene>("res://spring.tscn");
        for (int i = 0; i < spring_number; i++)
        {            
            Node2D s = scene.Instantiate<Node2D>();            
            AddChild(s);            
            springs.Add(s as spring);
 
            int x_position = distance_between_springs * i + 100;
            (s as spring).Initialize(x_position);
        }
 
        wave_position = new Curve2D();
    }
 
    public override void _PhysicsProcess(double delta)
    {
        if (Input.IsActionJustPressed("ui_accept"))
            splash(520);
 
        foreach (spring item in springs)
        {
            item.water_update(k, d);
        }
 
        List<float> left_deltas = new List<float>();
        List<float> right_deltas = new List<float>();
 
        for(int i = 0; i < springs.Count; i++)
        {
            left_deltas.Add(0.0f);
            right_deltas.Add(0.0f);
        }
 
        for (int p = 0; p < passes; p++)
        {
            for (int i = 0; i < springs.Count; i++)
            {
                if (i > 0)
                {
                    left_deltas[i] = spread * (springs[i].height - springs[i-1].height);
                    springs[i-1].velocity += left_deltas[i];
                }
                if (i < springs.Count - 1)
                {
                    right_deltas[i] = spread * (springs[i].height - springs[i+1].height);
                    springs[i+1].velocity += right_deltas[i];
                }
            }
        }
 
        draw_wave();
        draw_waterbody();
        // draw_wave()가 먼저 호출되어야 한다.
    }
 
    public void splash(int index, float speed)
    {
        if(index >= 0 && index < springs.Count)
        {
            springs[index].velocity += speed;
        }
    }
 
    public void draw_waterbody()
    {
        List<Vector2> surface_points = new List<Vector2>();
        //foreach (spring item in springs)
        //{
        //    surface_points.Add(item.Position);
        //}
        Vector2[] baked_points = wave_position.GetBakedPoints();
        for (int i = 0; i < baked_points.Length; i++)
        {
            surface_points.Add(baked_points[i]);
        }
        // wave_position으로 만든 곡선의 포인트를 폴리곤 포인트로 사용하기 위해
        // 위와 같이 수정한다.
 
        int first_index = 0;
        int last_index = surface_points.Count - 1;
 
        surface_points.Add(new Vector2(surface_points[last_index].X, bottom));
        surface_points.Add(new Vector2(surface_points[first_index].X, bottom));
        
        water_polygon.Polygon = surface_points.ToArray();
    }
 
    public override void _Draw()
    {
        DrawPolyline(wave_position.Tessellate(), Colors.Blue, 5);
    }
 
    public void draw_wave()
    {
        wave_position.ClearPoints();
 
        Vector2 control0 = new Vector2(-500);
        Vector2 control1 = new Vector2(500);
 
        foreach (spring item in springs)
        {
            wave_position.AddPoint(item.Position, control0, control1);
        }
 
        QueueRedraw();
    }
}
 

 

 

WaterBody.cs를 위와같이 변경한다.

 

폴리곤도 부드럽게 바뀌었다.

 

2024.02.11 - [Godot] - [Godot] 2D Splash Dynamic Wave 자연스러운 물결 파동 3. 응용

 

※ 참고

Beziers, curves, and paths

Make a Splash With Dynamic 2D Water Effects

 

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

자연스러운 2D 물결 파동을 만들어 보자.

 

우선 저항이 없는 완전 탄성 스프링 운동을 시뮬레이션 해 보자.

 

Node2D, Sprite2D를 이용해 스프링으로 사용할 씬을 구성하고 스크립트를 추가한다.

 

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
using Godot;
 
public partial class spring : Node2D
{
    public float velocity;
    public float force;
    public float height;
    public float target_height;
    public float k;
    
    public override void _Ready()
    {
        target_height = Position.Y + 200;
        // 목표 위치
        k = 0.0015f;
        // 스프링 탄성 계수. 탄성 계수가 클수록 빠르게
        // 움직인다. 운동 거리와는 상관 없다.
    }
 
    public override void _PhysicsProcess(double delta)
    {
        water_update(k);        
    }
 
    public void water_update(float spring_constant)
    {
        height = Position.Y;
        float x = height - target_height;
        // 변위. 스프링 운동시 실제 이동 거리는 -x ~ +x 다.
 
        force = -spring_constant * x;
        // 훅의 법칙(Hooke's Law) F = -kx
        // 스프링의 복원력과 변형량 사이에는 비례관계가 성립한다.
        // F = ma, a = -kx/m
        // -k/m = c(상수), 가속도 a는 x에 비례한다.
 
        velocity += force;
        // 속도에 힘을 누적시킨다.
        // 이동거리(s) = 속도(v) X 시간(t)
        // 매 순간의 속도를 모두 더하면 총 이동거리를 알 수 있다.
        // 이 예에서는 아래 Position 값이 매 순간의 총 이동거리다.
 
        Position += new Vector2(0, velocity);
        // 스프링은 목표 높이(Position.Y + 200)를 중심으로 변위 x만큼
        // (-x ~ +x) 왕복 운동한다. 목표 높이가 스프링을 늘리거나
        // 압축하지 않은 평상시 위치다.
        // 예) 초기 위치(Position)의 Y값이 100 이면, 300(100+200)이
        // 평상시 위치고 100(300-200)에서 500(300+200)사이의 왕복
        // 운동을 한다.
    }
}
 

 

 

추가한 스크립트에 스프링 운동을 표현할 코드를 작성한다.

 

스프링을 사용할 씬을 구성한다. Node2D에 위에서 만든 스프링 씬을 추가한다.

 

프로그램을 실행하면 스프링 운동을 한다.

 

 

비탄성 스프링 운동을 시뮬레이션 해 보자.

 

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
using Godot;
 
public partial class spring : Node2D
{
    public float velocity;
    public float force;
    public float height;
    public float target_height;
    public float k;
    public float d;
 
    public override void _Ready()
    {
        target_height = Position.Y + 200;
        // 목표 위치
        k = 0.0015f;
        // 스프링 탄성 계수. 탄성 계수가 클수록 빠르게
        // 움직인다. 운동 거리와는 상관 없다.
        d = 0.005f;
        // 저항 계수
    }
 
    public override void _PhysicsProcess(double delta)
    {
        water_update(k, d);        
    }
 
    public void water_update(float spring_constant, float dampening)
    {
        height = Position.Y;
        float x = height - target_height;
        // 변위. 스프링 운동시 실제 이동 거리는 -x ~ +x 다.
 
        float resist = -dampening * velocity;
        // 속도에 비례하는 저항값을 계산한다.
 
        force = -spring_constant * x + resist;
        // 훅의 법칙(Hooke's Law) F = -kx
        // 스프링의 복원력과 변형량 사이에는 비례관계가 성립한다.
        // F = ma, a = -kx/m
        // -k/m = c(상수), 가속도 a는 x에 비례한다.
        // 그리고 저항을 더해주면 힘이 점점 감소한다.
 
        velocity += force;
        // 속도에 힘을 누적시킨다.
        // 이동거리(s) = 속도(v) X 시간(t)
        // 매 순간의 속도를 모두 더하면 총 이동거리를 알 수 있다.
        // 이 예에서는 아래 Position 값이 매 순간의 총 이동거리다.
 
        Position += new Vector2(0, velocity);
        // 스프링은 목표 높이(Position.Y + 200)를 중심으로 변위 x만큼
        // (-x ~ +x) 왕복 운동한다. 목표 높이가 스프링을 늘리거나
        // 압축하지 않은 평상시 위치다.
        // 예) 초기 위치(Position)의 Y값이 100 이면, 300(100+200)이
        // 평상시 위치고 100(300-200)에서 500(300+200)사이의 왕복
        // 운동을 한다.
    }
}
 

 

 

저항이 추가된 코드를 작성한다.

 

저항이 적용돼 움직임이 점점 감소한다.

 

스프링을 여러개 만들고 서로 영향을 주고 받게 해 보자.

 

스프링 스프라이트를 적당한 크기로 조절하고 스크립트(spring.cs)는 아래와 같이 수정한다.

 

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
using Godot;
 
public partial class spring : Node2D
{
    public float velocity;
    public float force;
    public float height;
    public float target_height;
 
    public void Initialize()
    {
        height = Position.Y;
        target_height = Position.Y;
        velocity = 0;
    }
 
    public void water_update(float spring_constant, float dampening)
    {
        height = Position.Y;
        // 매번 높이값을 업데이트 한다.
 
        float x = height - target_height;
        float resist = -dampening * velocity;
        force = -spring_constant * x + resist;
        
        velocity += force;
        Position += new Vector2(0, velocity);
    }
}
 

 

 

 

새로운 씬을 만들고 스프링을 추가한다. 스크립트(WaterBody.cs)를 추가하고 아래와 같이 작성한다.

 

 

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
using Godot;
using System.Collections.Generic;
 
public partial class WaterBody : Node2D
{
    public float k;
    public float d;
    public float spread;
    // 파동이 옆으로 퍼지는 정도
    public int passes;
    // 파동의 영향력
    public List<spring> springs;
    // 파동을 시뮬레이션 할 스프링 리스트
 
    public override void _Ready()
    {
        k = 0.015f;
        d = 0.05f;
        spread = 0.0002f;
        passes = 8;
        springs= new List<spring>();
 
        foreach(spring item in GetChildren())
        {
            springs.Add(item);
            item.Initialize();
        }
        
        //splash(2, 10);
    }
 
    public override void _PhysicsProcess(double delta)
    {
        if (Input.IsActionJustPressed("ui_accept"))
            splash(210);
        // 엔터키를 누르면 물결 효과가 진행된다.
 
        foreach (spring item in springs)
        {
            item.water_update(k, d);
        }
 
        List<float> left_deltas = new List<float>();
        // 각 스프링의 왼쪽 스프링과의 높이 차이 리스트
        List<float> right_deltas = new List<float>();
        // 각 스프링의 오른쪽 스프링과의 높이 차이 리스트
 
        for(int i = 0; i < springs.Count; i++)
        {
            left_deltas.Add(0.0f);
            right_deltas.Add(0.0f);
            // 모든 차이를 0으로 초기화.
        }
 
        for (int p = 0; p < passes; p++)
        {
            for (int i = 0; i < springs.Count; i++)
            {
                if (i > 0)
                {
                    left_deltas[i] = spread * (springs[i].height - springs[i-1].height);
                    springs[i-1].velocity += left_deltas[i];
                    // 높이 차이와 파동 전달 정도(spread)를 곱하여 왼쪽 스프링 속도에 누적시킨다.
                }
                if (i < springs.Count - 1)
                {
                    right_deltas[i] = spread * (springs[i].height - springs[i+1].height);
                    springs[i+1].velocity += right_deltas[i];
                    // 높이 차이와 파동 전달 정도(spread)를 곱하여 오른쪽 스프링 속도에 누적시킨다.
                }
            }
        }        
    }
 
    public void splash(int index, float speed)
    {
        if(index >= 0 && index < springs.Count)
        {
            springs[index].velocity += speed;
        }
        // index번 스프링에 속도를 지정하여 물에 돌을 던진 것 같은 효과를 준다.
    }
}
 

 

 

메인 씬에 WaterBody씬을 추가한다.

 

실행하고 엔터키를 누르면 가운데 스프링의 움직임이 다른 스프링에 전달된다.

 

 

이제 스프링을 스크립트에서 인스턴스화 하고 초기화 되도록 해 보자.

 

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
using Godot;
 
public partial class spring : Node2D
{
    public float velocity;
    public float force;
    public float height;
    public float target_height;
 
    public void Initialize(int x_position)
    {
        Position = new Vector2(x_position, 100);
        // 인수로 받은 x 좌표와 y=100으로 위치를 정한다.
        height = Position.Y;
        target_height = Position.Y;
        velocity = 0;        
    }
 
    public void water_update(float spring_constant, float dampening)
    {
        height = Position.Y;
        float x = height - target_height;
        float resist = -dampening * velocity;
        force = -spring_constant * x + resist;
        
        velocity += force;
        Position += new Vector2(0, velocity);
    }
}
 

 

 

spring.cs 는 위와 같이 변경한다.

 

WaterBody 씬은 스프링 노드를 모두 삭제한다.

 

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
using Godot;
using System.Collections.Generic;
 
public partial class WaterBody : Node2D
{
    public float k;
    public float d;
    public float spread;
    public int passes;
    public List<spring> springs;
    public int spring_number;
    public int distance_between_springs;
    public PackedScene scene;
 
    public override void _Ready()
    {
        k = 0.015f;
        d = 0.05f;
        spread = 0.0002f;
        passes = 8;
        springs= new List<spring>();
        spring_number = 10;
        // 스프링 갯수
        distance_between_springs = 100;
        // 스프링 사이 간격
        scene = ResourceLoader.Load<PackedScene>("res://spring.tscn");
        // 스프링 씬을 로드한다.
        for (int i = 0; i < spring_number; i++)
        {            
            Node2D s = scene.Instantiate<Node2D>();            
            AddChild(s);            
            springs.Add(s as spring);
            // 로드한 스프링 씬을 인스턴스화 하고 자식노드 및 리스트에 추가한다.
 
            int x_position = distance_between_springs * i + 100;
            (s as spring).Initialize(x_position);
            // 인스턴스된 스프링에 적당한 위치를 인수로 주고 초기화한다.
        }
    }
 
    public override void _PhysicsProcess(double delta)
    {
        if (Input.IsActionJustPressed("ui_accept"))
            splash(520);
 
        foreach (spring item in springs)
        {
            item.water_update(k, d);
        }
 
        List<float> left_deltas = new List<float>();
        List<float> right_deltas = new List<float>();
 
        for(int i = 0; i < springs.Count; i++)
        {
            left_deltas.Add(0.0f);
            right_deltas.Add(0.0f);
        }
 
        for (int p = 0; p < passes; p++)
        {
            for (int i = 0; i < springs.Count; i++)
            {
                if (i > 0)
                {
                    left_deltas[i] = spread * (springs[i].height - springs[i-1].height);
                    springs[i-1].velocity += left_deltas[i];
                }
                if (i < springs.Count - 1)
                {
                    right_deltas[i] = spread * (springs[i].height - springs[i+1].height);
                    springs[i+1].velocity += right_deltas[i];
                }
            }
        }        
    }
 
    public void splash(int index, float speed)
    {
        if(index >= 0 && index < springs.Count)
        {
            springs[index].velocity += speed;
        }
    }
}
 

 

 

WaterBody.cs 는 위와 같이 변경한다.

 

Main에서 플레이 버튼을 클릭한다.

 

 

엔터키를 누르면 6번째 스프링의 움직임이 양 옆으로 전달된다.

 

2023.10.18 - [Godot] - [Godot] 2D Splash Dynamic Wave 자연스러운 물결 파동 2. 구현

 

※ 참고

Make a Splash With Dynamic 2D Water Effects

 

 

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

ParallaxBackground와 ParallaxLayer를 이용해 배경 화면을 자연스럽게 스크롤 해 보자.

 

Project Settings - Rendering - Textures - Canvas Textures - Default Texture Filter - Nearest 를 선택한다.

 

위와 같이 씬을 구성한다.

● ParallaxBackground - 두 개의 ParallaxLayer를 자식 노드로 생성하고 스크립트를 추가한다.

● ParallaxLayer - 각각 Sprite2D를 자식 노드로 생성한다.

● Sprite2D - 배경 화면을 Texture로 지정한다.

 

background_layer_1.png
0.01MB
background_layer_2.png
0.01MB

 

두 개의 ParallaxLayer 모두 Motion - Mirroring - X 속성을 스프라이트 크기에 맞게 지정한다.

수평으로 스크롤 하는 경우 X 속성을 지정하지만 수직이라면 Y 속성을 지정한다.

 

1
2
3
4
5
6
7
8
9
10
11
using Godot;
 
public partial class BackgroundScript : ParallaxBackground
{
    public int ScrollingSpeed = 100;
 
    public override void _Process(double delta)
    {
        ScrollOffset -= new Vector2(ScrollingSpeed * (float)delta, 0);
    }
}
 

 

ParallaxBackground 스크립트를 작성한다.

 

 

두 배경이 같은 속도로 이동한다.

 

멀리 있는 배경(ParallaxLayer)의 Motion - Scale 속성을 0.5로 지정한다.

 

멀리 있는 배경이 느리게 이동한다.

 

※ 참고

ParallaxBackground

ParallaxLayer

 

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

기본적인 파티클 시스템 사용.

 

GPUParticles2D를 추가하고 Process Material에 ParticleProcessMaterial을 생성한다.

 

확대해보면 파티클이 생성되고 있다.

 

눈이 내리는 효과를 만들어 보자.

 

Emission Shape과 Gravity 속성을 위와 같이 변경한다.

 

Turbulence, Time - Lifetime 속성을 위와 같이 변경한다.

 

 

자연스럽게 눈이 내린다.

 

이번엔 비가 내리는 효과를 만들어 보자.

 

Direction, Gravity, Initial Velocity 속성을 위와 같이 변경한다.

 

Turbulence를 비활성화하고 Trails 속성을 활성화 하고 Lifetime을 적장히 조절한다.

 

비가 내린다.

 

 

Emission Mask를 사용해 특정 영역에서 파티클을 생성해 보자.

 

파티클 노드를 선택하고 GPUParticles2D - Load Emission Mask를 선택한다.

 

letter.png
0.01MB

 

마스크로 사용할 이미지를 선택한다.

 

Solid Pixels를 선택하고 OK를 클릭한다.

 

Amount, Direction, Initial Velocity 속성을 조절한다.

 

 

Turbulence, Time - Lifetime 속성을 조절한다.

 

Solid Pixels

 

Border Pixels

 

Directed Border Pixels

 

 

Color, Hue Variation 속성을 조절한다.

 

 

Texture를 지정하고 그에 맞게 Scale 속성을 설정한다.

 

flare-lens.png
0.31MB

 

 

※ 참고

2D particle Systems

 

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

2D 네비게이션 기본 사용법

 

2D 네비게이션을 사용하기 위해 세팅한다.

● NavigationRegion2D - Navigation Polygon을 생성하고 NavigationAgent2D가 이동 가능한 공간을 적당히 그린다.

● CharacterBody2D - 스크립트를 생성한다.

   Sprite2D - 스프라이트 사이즈를 적당히 조절한다.

   CollisionShape2D - 스프라이트 사이즈에 맞게 적당히 조절한다.

   NavigationAgent2D - Debug - Enabled - On 체크한다.

 

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
using Godot;
 
public partial class Nav : CharacterBody2D
{
    public float movement_speed = 300.0f;
    public Vector2 movement_target_position = new Vector2(950500);
    public NavigationAgent2D navigation_agent;
 
    public override void _Ready()
    {
        navigation_agent = GetNode<NavigationAgent2D>("NavigationAgent2D");
 
        // These values need to be adjusted for the actor's speed
        // and the navigation layout.
        navigation_agent.PathDesiredDistance = 4.0f;
        navigation_agent.TargetDesiredDistance = 4.0f;
 
        // Make sure to not await during _ready.
        CallDeferred("actor_setup");
        // Calls the method on the object during idle time. Always returns null,
        // not the method's result.
    }
 
    public async void actor_setup()
    {
        // Wait for the first physics frame so the NavigationServer can sync.
        // On the first frame the NavigationServer map has not synchronized region
        // data and any path query will return empty. Await one frame to pause
        // scripts until the NavigationServer had time to sync.
        
        //await ToSignal(GetTree(), "physics_frame");
        await ToSignal(GetTree(), SceneTree.SignalName.PhysicsFrame);        
        // physics_frame
        // Emitted immediately before Node._physics_process is called on every node
        // in the SceneTree.
 
        // Now that the navigation map is no longer empty, set the movement target.
        set_movement_target(movement_target_position);
    }
 
    public void set_movement_target(Vector2 movement_target)
    {
        navigation_agent.TargetPosition = movement_target;
    }
 
    public override void _PhysicsProcess(double delta)
    {        
        if (navigation_agent.IsNavigationFinished())
        {
            return;
        }
 
        //Vector2 current_agent_position = GlobalPosition;
        //Vector2 next_path_position = navigation_agent.GetNextPathPosition();
        //Vector2 new_velocity = next_path_position - current_agent_position;
        Vector2 new_velocity = ToLocal(navigation_agent.GetNextPathPosition());
 
        new_velocity = new_velocity.Normalized();
        new_velocity = new_velocity * movement_speed;
 
        Velocity = new_velocity;
        MoveAndSlide();
    }
}
 

 

 

CharacterBody2D에 추가한 스크립트에 2D 네비게이션 사용 코드를 작성한다.

movement_target_position은 목표 지점을 적당히 지정한다.

 

목표지점까지 캐릭터가 진행하는 경로가 표시되며 이동한다.

 

이번엔 마우스 포인터를 따라가도록 해 보자.

 

Navigation Polygon을 수정한다.

 

 

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
using Godot;
 
public partial class Nav : CharacterBody2D
{
    public float movement_speed = 200.0f;
    public NavigationAgent2D navigation_agent;
 
    public override void _Ready()
    {
        navigation_agent = GetNode<NavigationAgent2D>("NavigationAgent2D");
 
        navigation_agent.PathDesiredDistance = 4.0f;
        navigation_agent.TargetDesiredDistance = 4.0f;
    }
 
    public override void _Process(double delta)
    {
        navigation_agent.TargetPosition = GetGlobalMousePosition();
    }
 
    public override void _PhysicsProcess(double delta)
    {
        if (navigation_agent.IsNavigationFinished())
        {
            return;
        }
 
        Vector2 new_velocity = ToLocal(navigation_agent.GetNextPathPosition());
        new_velocity = new_velocity.Normalized();
        new_velocity = new_velocity * movement_speed;
 
        Velocity = new_velocity;
 
        MoveAndSlide();
    }
}
 

 

 

마우스 포인터를 따라가는 코드를 작성한다.

 

Navigation Polygon 내에서 마우스를 따라 움직인다.

 

이번엔 타일맵을 이용해 맵을 만들고 길을 찾아보자.

Tile-Sets (64-64).png
0.01MB

 

NavigationRegion2D를 삭제하고 TileMap을 추가해 위와 같이 맵을 만든다.

TileMap에 Physics Layer와 Navigation Layer를 하나씩 추가한다. 각 타일의 설정은 아래 내용을 참고한다.

 

여러 타일 중 두 가지만(벽과 길) 사용해 맵을 만들어 보자. 벽으로 사용할 타일에 Physics(Collision) Polygon을 생성한다.

 

 

길로 사용할 타일에 Navigation Polygon을 생성한다.

 

CharacterBody2D의 설정도 바꿔야 한다.

● Motion Mode - Floating

● Wall Min Slide Angle - 0

● 자식노드의 CollisionShape2D - Shape이 사각형이라면 원으로 바꾼다.

이렇게 변경하지 않으면 캐릭터가 이동 중 모서리에 걸려 움직이지 못하는 경우가 발생한다.

스크립트는 변경할 필요 없다.

 

게임을 실행하면 마우스를 따라 캐릭터가 길을 찾고 이동한다.

 

※ 참고

2D Navigation Overview

Using NavigationAgents

 

반응형
Posted by J-sean
:

[Godot] Wall Jump 벽 점프

Godot 2023. 10. 2. 16:53 |
반응형

벽을 타고 점프해 보자.

 

바닥, 벽, 캐릭터를 적당히 배치한다. 캐릭터에는 스크립트를 추가하자.

 

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
using Godot;
 
public partial class CharacterBody2D : Godot.CharacterBody2D
{
    public const float Speed = 300.0f;
    public const float JumpVelocity = -400.0f;
 
    public float gravity = ProjectSettings.GetSetting("physics/2d/default_gravity").AsSingle();
 
    public override void _PhysicsProcess(double delta)
    {
        Vector2 velocity = Velocity;
 
        if (!IsOnFloor())
            velocity.Y += gravity * (float)delta;
        
        if (Input.IsActionJustPressed("ui_accept"&& IsOnFloor())
            velocity.Y = JumpVelocity;
 
        if (Input.IsActionJustPressed("ui_accept"&& IsOnWallOnly())
            velocity.Y = JumpVelocity;
 
        Vector2 direction = Input.GetVector("ui_left""ui_right""ui_up""ui_down");
        if (direction != Vector2.Zero)
        {
            velocity.X = direction.X * Speed;
        }
        else
        {
            velocity.X = Mathf.MoveToward(Velocity.X, 0, Speed);
        }
 
        Velocity = velocity;
        MoveAndSlide();
    }
}
 

 

 

IsOnWallOnly() 함수를 사용한 스크립트를 작성한다. 

 

벽을 타고 wall jump가 가능하다.

 

※ 참고

CharacterBody2D

Using CharacterBody2D/3D

 

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

타일맵에 Collision Layer, Collision Mask를 설정하고 게임 실행 중 코드에서 Custom Data에 따라 적절히 바꿀 수 있다.

 

타일맵을 준비한다.

 

TileMap - Tile Set- Physics Layers - Collision Layer/Mask를 설정한다.

 

TileMap - Tile Set - Custom Data Layers에 원하는 데이터 이름과 타입을 설정한다.

 

Custom Data를 지정할 타일을 선택하고 Custom Data에 원하는 데이터 값을 설정한다. 위에서 bool 타입을 설정했기 때문에 On(True)이 표시된다. (0은 첫 번째 타일맵 레이어를 뜻한다)

 

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
using Godot;
 
public partial class player : Node2D
{
    [Export]
    CharacterBody2D character;
 
    public override void _Ready()
    {
        //character = GetNode<CharacterBody2D>("CharacterBody2D");
    }
 
    public override void _Process(double delta)
    {
        Vector2I mousePosition = GetNode<TileMap>("TileMap").LocalToMap(GetViewport().GetMousePosition());
        // 마우스 커서 위치를 타일맵 좌표로 변환한다.
        int layers = GetNode<TileMap>("TileMap").GetLayersCount();
        // 타일맵 레이어 갯수를 가져온다.
 
        for (int layer = 0; layer < layers; layer++)
        {
            TileData td = GetNode<TileMap>("TileMap").GetCellTileData(layer, mousePosition);
            // 마우스가 위치한 타일의 타일 데이터를 가져온다.
            if (td != null)
            {
                GD.Print($"Layer: {layer}");
                GD.Print($"CustomData: {td.GetCustomData("extraData")}");
                // TileMap - Inspector - Custom Data Layers 에서 추가한 변수를
                // TileSet탭 - Tiles탭 - Select - Custom Data 에서 세팅한 값.
 
                //GetNode<TileMap>("TileMap").TileSet.SetPhysicsLayerCollisionLayer(layer, 8);
                //GetNode<TileMap>("TileMap").TileSet.SetPhysicsLayerCollisionMask(layer, 16);
                // extraData 값을 확인하고 그에 맞게 레이어 값을 바꿀 수 있다.
 
                //int groundLayer = 1;
                //int buildingLayer = 2;
                //character.SetCollisionLayerValue(groundLayer, false);
                //character.SetCollisionMaskValue(buildingLayer, true);
                // 아니면 상화에 따라 캐릭터의 collision layer/mask 값을 바꿀 수 있다.
                // 주의할 점은 SetCollisionXXXValue()의 레이어 값은 1~32이므로 미리 변경하고 싶은
                // 레이어 번호를 변수에 저장하고 사용하자. 0 이 들어가면 안된다.
            }
            else
            {
                GD.Print("NULL");
            }
 
            GD.Print($"Collision Layer:" +
                $"{GetNode<TileMap>("TileMap").TileSet.GetPhysicsLayerCollisionLayer(layer)}");
            GD.Print($"Collision Mask:" +
                $"{GetNode<TileMap>("TileMap").TileSet.GetPhysicsLayerCollisionMask(layer)}");
            // 1번 레이어(마스크) value: 1
            // 2번 레이어(마스크) value: 2
            // 3번 레이어(마스크) value: 4
            // 4번 레이어(마스크) value: 8
            // ... 9번 레이어(마스크) value: 256 ...
        }
    }
}
 

 

 

스크립트를 작성한다.

 

게임을 실행하면 마우스 위치에 따라 정보가 표시된다.

 

반응형

'Godot' 카테고리의 다른 글

[Godot] 2D Navigation Basic Setup  (0) 2023.10.06
[Godot] Wall Jump 벽 점프  (0) 2023.10.02
[Godot] RayCast2D C# Example  (0) 2023.09.27
[Godot] Area2D Gravity 중력  (0) 2023.09.27
[Godot] AddChild(), RemoveChild()  (0) 2023.09.26
Posted by J-sean
:

[Godot] RayCast2D C# Example

Godot 2023. 9. 27. 17:45 |
반응형

RayCast2D C# 예제.

 

씬을 준비하고 RayCast2D와 스크립트를 추가한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    public override void _Draw()
    {
        DrawLine(GetNode<RayCast2D>("RayCast2D").Position,
            GetNode<RayCast2D>("RayCast2D").Position +
            GetNode<RayCast2D>("RayCast2D").TargetPosition,
            new Color(1000.5f), 5);
        // Ray를 확인할 수 있도록 선으로 표시한다.
    }
 
    public override void _PhysicsProcess(double delta)
    {    
        if (GetNode<RayCast2D>("RayCast2D").IsColliding())
        {
            Node obj = GetNode<RayCast2D>("RayCast2D").GetCollider() as Node;            
            GD.Print(obj.Name);
            // Ray와 충돌한 오브젝트의 이름을 출력한다.
        }
   ...
 

 

 

스크립트를 작성하고 실행하면 Ray와 충돌하는 오브젝트의 이름이 출력된다.

 

※ 참고

RayCast2D

 

반응형
Posted by J-sean
: