first commit
This commit is contained in:
206
scripts/die.gd
Normal file
206
scripts/die.gd
Normal file
@@ -0,0 +1,206 @@
|
||||
extends RigidBody3D
|
||||
class_name Die
|
||||
## Ein physikalischer Würfel mit abgerundeten Kanten.
|
||||
## Der Wert wird aus der Ruhelage abgelesen.
|
||||
|
||||
const SIZE := 0.15
|
||||
const HALF := SIZE * 0.5
|
||||
const CORNER := SIZE * 0.12 ## Rundungsradius der Kanten und Ecken
|
||||
const SEGMENTS := 6 ## Unterteilung je Würfelseite
|
||||
const PIP_R := SIZE * 0.11 ## Radius eines Auges
|
||||
const PIP_SEGS := 8
|
||||
|
||||
const COL_BODY := Color(0.94, 0.92, 0.86)
|
||||
const COL_PIP := Color(0.03, 0.03, 0.04)
|
||||
|
||||
## Lokale Achse -> aufgedruckter Wert (gegenüberliegende Seiten ergeben 7)
|
||||
const FACE_AXES: Array[Vector3] = [
|
||||
Vector3.RIGHT, Vector3.LEFT, Vector3.UP, Vector3.DOWN, Vector3.BACK, Vector3.FORWARD
|
||||
]
|
||||
const FACE_VALUES: Array[int] = [1, 6, 2, 5, 3, 4]
|
||||
|
||||
const PIPS := {
|
||||
1: [Vector2(0, 0)],
|
||||
2: [Vector2(-1, -1), Vector2(1, 1)],
|
||||
3: [Vector2(-1, -1), Vector2(0, 0), Vector2(1, 1)],
|
||||
4: [Vector2(-1, -1), Vector2(-1, 1), Vector2(1, -1), Vector2(1, 1)],
|
||||
5: [Vector2(-1, -1), Vector2(-1, 1), Vector2(0, 0), Vector2(1, -1), Vector2(1, 1)],
|
||||
6: [Vector2(-1, -1), Vector2(-1, 0), Vector2(-1, 1),
|
||||
Vector2(1, -1), Vector2(1, 0), Vector2(1, 1)],
|
||||
}
|
||||
|
||||
# Körper und Augen stecken in einem gemeinsamen Mesh mit Vertexfarben,
|
||||
# damit ein Würfel nur ein einziges Renderobjekt ist.
|
||||
static var _mesh: ArrayMesh
|
||||
static var _mat: StandardMaterial3D
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
mass = 0.03
|
||||
contact_monitor = false
|
||||
can_sleep = true
|
||||
linear_damp = 0.25
|
||||
angular_damp = 0.35
|
||||
|
||||
var pm := PhysicsMaterial.new()
|
||||
pm.friction = 0.6
|
||||
pm.bounce = 0.25
|
||||
physics_material_override = pm
|
||||
|
||||
var shape := CollisionShape3D.new()
|
||||
var box := BoxShape3D.new()
|
||||
box.size = Vector3.ONE * SIZE
|
||||
shape.shape = box
|
||||
add_child(shape)
|
||||
|
||||
_build_visuals()
|
||||
|
||||
|
||||
# ------------------------------------------------------------ Aufbau
|
||||
|
||||
static func _face_tangents(n: Vector3) -> Array:
|
||||
var u := Vector3.UP if absf(n.y) < 0.9 else Vector3.RIGHT
|
||||
u = (u - n * u.dot(n)).normalized()
|
||||
return [u, n.cross(u)]
|
||||
|
||||
|
||||
## Schiebt einen Punkt der Würfeloberfläche auf die abgerundete Form
|
||||
## (Minkowski-Summe aus kleinerem Würfel und Kugel).
|
||||
static func _round_point(p: Vector3) -> Vector3:
|
||||
var inner := HALF - CORNER
|
||||
var c := Vector3(clampf(p.x, -inner, inner), clampf(p.y, -inner, inner),
|
||||
clampf(p.z, -inner, inner))
|
||||
var d := p - c
|
||||
if d.length_squared() < 1e-9:
|
||||
return p
|
||||
return c + d.normalized() * CORNER
|
||||
|
||||
|
||||
static func _build_mesh() -> ArrayMesh:
|
||||
var st := SurfaceTool.new()
|
||||
st.begin(Mesh.PRIMITIVE_TRIANGLES)
|
||||
for f in FACE_AXES.size():
|
||||
var n: Vector3 = FACE_AXES[f]
|
||||
var t := _face_tangents(n)
|
||||
st.set_color(COL_BODY)
|
||||
for i in SEGMENTS:
|
||||
for j in SEGMENTS:
|
||||
var quad: Array[Vector3] = []
|
||||
for c: Vector2 in [Vector2(i, j), Vector2(i + 1, j),
|
||||
Vector2(i + 1, j + 1), Vector2(i, j + 1)]:
|
||||
var u := (c.x / SEGMENTS) * 2.0 - 1.0
|
||||
var v := (c.y / SEGMENTS) * 2.0 - 1.0
|
||||
quad.append(_round_point(n * HALF + t[0] * u * HALF + t[1] * v * HALF))
|
||||
for k in [0, 2, 1, 0, 3, 2]:
|
||||
st.add_vertex(quad[k])
|
||||
_add_pips(st, n, t, FACE_VALUES[f])
|
||||
st.index()
|
||||
st.generate_normals()
|
||||
return st.commit()
|
||||
|
||||
|
||||
## Augen als flache Scheiben knapp über der Seitenfläche, gleiche Surface.
|
||||
static func _add_pips(st: SurfaceTool, n: Vector3, t: Array, value: int) -> void:
|
||||
st.set_color(COL_PIP)
|
||||
var spread := HALF * 0.50
|
||||
var lift: Vector3 = n * (HALF + 0.0012)
|
||||
for p: Vector2 in PIPS[value]:
|
||||
var mid: Vector3 = lift + t[0] * p.x * spread + t[1] * p.y * spread
|
||||
var ring: Array[Vector3] = []
|
||||
for k in PIP_SEGS:
|
||||
var a := TAU * k / PIP_SEGS
|
||||
ring.append(mid + (t[0] * cos(a) + t[1] * sin(a)) * PIP_R)
|
||||
for k in PIP_SEGS:
|
||||
st.add_vertex(mid)
|
||||
st.add_vertex(ring[(k + 1) % PIP_SEGS])
|
||||
st.add_vertex(ring[k])
|
||||
|
||||
|
||||
func _build_visuals() -> void:
|
||||
if _mesh == null:
|
||||
_mesh = _build_mesh()
|
||||
_mat = StandardMaterial3D.new()
|
||||
_mat.vertex_color_use_as_albedo = true
|
||||
# Kamera und Deckenlicht stehen beide senkrecht: bei glatter Oberfläche
|
||||
# liegt der Glanzpunkt genau auf der Fläche und hellt die Augen auf.
|
||||
_mat.roughness = 0.9
|
||||
_mat.metallic_specular = 0.0
|
||||
var body := MeshInstance3D.new()
|
||||
body.mesh = _mesh
|
||||
body.material_override = _mat
|
||||
add_child(body)
|
||||
|
||||
|
||||
# ------------------------------------------------------------ Zustand
|
||||
|
||||
## Wert der aktuell nach oben zeigenden Seite.
|
||||
func top_value() -> int:
|
||||
var best := -2.0
|
||||
var val := 1
|
||||
for i in FACE_AXES.size():
|
||||
var d: float = (global_transform.basis * FACE_AXES[i]).y
|
||||
if d > best:
|
||||
best = d
|
||||
val = FACE_VALUES[i]
|
||||
return val
|
||||
|
||||
|
||||
## Wie sauber der Würfel liegt (1.0 = perfekt flach).
|
||||
func flatness() -> float:
|
||||
var best := -2.0
|
||||
for a in FACE_AXES:
|
||||
best = maxf(best, (global_transform.basis * a).y)
|
||||
return best
|
||||
|
||||
|
||||
func is_settled() -> bool:
|
||||
return sleeping or (linear_velocity.length() < 0.06 and angular_velocity.length() < 0.15)
|
||||
|
||||
|
||||
func throw_from(origin: Vector3, rng: RandomNumberGenerator) -> void:
|
||||
freeze = false
|
||||
sleeping = false
|
||||
var t := Transform3D()
|
||||
t.basis = Basis(Vector3(rng.randf(), rng.randf(), rng.randf()).normalized(), rng.randf() * TAU)
|
||||
t.origin = origin
|
||||
global_transform = t
|
||||
linear_velocity = Vector3(rng.randf_range(-1.5, 1.5), rng.randf_range(-0.3, 0.0),
|
||||
rng.randf_range(-1.5, 1.5))
|
||||
angular_velocity = Vector3(rng.randf_range(-24, 24), rng.randf_range(-24, 24),
|
||||
rng.randf_range(-24, 24))
|
||||
|
||||
|
||||
func nudge(rng: RandomNumberGenerator) -> void:
|
||||
sleeping = false
|
||||
apply_central_impulse(Vector3(rng.randf_range(-0.04, 0.04), 0.06, rng.randf_range(-0.04, 0.04)))
|
||||
apply_torque_impulse(Vector3(rng.randf_range(-0.008, 0.008), rng.randf_range(-0.008, 0.008),
|
||||
rng.randf_range(-0.008, 0.008)))
|
||||
|
||||
|
||||
## Dreht den Würfel so, dass `value` oben liegt (für Booster-Effekte).
|
||||
func snap_to_value(value: int, rng: RandomNumberGenerator) -> void:
|
||||
var idx := FACE_VALUES.find(value)
|
||||
if idx < 0:
|
||||
return
|
||||
var a: Vector3 = FACE_AXES[idx]
|
||||
var b := Vector3.RIGHT if absf(a.x) < 0.9 else Vector3.UP
|
||||
b = (b - a * b.dot(a)).normalized()
|
||||
var c := a.cross(b)
|
||||
|
||||
var yaw := rng.randf() * TAU
|
||||
var wb := Vector3(cos(yaw), 0.0, sin(yaw))
|
||||
var wa := Vector3.UP
|
||||
var wc := wa.cross(wb)
|
||||
|
||||
var t := global_transform
|
||||
t.basis = Basis(wb, wa, wc) * Basis(b, a, c).transposed()
|
||||
global_transform = t
|
||||
if not freeze:
|
||||
sleeping = false
|
||||
linear_velocity = Vector3.ZERO
|
||||
angular_velocity = Vector3.ZERO
|
||||
|
||||
|
||||
func set_kinematic(on: bool) -> void:
|
||||
freeze_mode = RigidBody3D.FREEZE_MODE_KINEMATIC
|
||||
freeze = on
|
||||
Reference in New Issue
Block a user