CARLA
Loading...
Searching...
No Matches
CarlaLightSubsystem.cpp
Go to the documentation of this file.
1// Copyright (c) 2026 Computer Vision Center (CVC) at the Universitat Autonoma
2// de Barcelona (UAB).
3//
4// This work is licensed under the terms of the MIT license.
5// For a copy, see <https://opensource.org/licenses/MIT>.
6
9
10#include <util/ue-header-guard-begin.h>
11#include "Kismet/GameplayStatics.h"
12#include "TimerManager.h"
13#include <util/ue-header-guard-end.h>
14
15//using cr = carla::rpc;
16
17void UCarlaLightSubsystem::Initialize(FSubsystemCollectionBase& Collection)
18{
19 // TODO: Subscribe to map change
20}
21
26
27void UCarlaLightSubsystem::RegisterLight(UCarlaLight* CarlaLight)
28{
29 if(CarlaLight)
30 {
31 // OnRegister and BeginPlay both register: the same light coming back
32 // keeps its entry. Taken for a clone below, it got a second id on every
33 // call and the stale entries were never switched (World Partition maps
34 // ended up with ~3 entries per lamp).
35 if (const int* ExistingId = Lights.FindKey(CarlaLight))
36 {
37 CarlaLight->SetId(*ExistingId);
38 }
39 auto LightId = CarlaLight->GetId();
40 if (Lights.Contains(LightId) && Lights[LightId] != CarlaLight)
41 {
42 // Runtime-spawned lights cloned from a template (PCG Spawn Actor,
43 // duplicated actors...) all arrive carrying the template's id;
44 // dropping them here silently disconnected every clone from the
45 // subsystem (day/night events, client light API). Assign the next
46 // free id instead.
47 while (Lights.Contains(LightId))
48 {
49 ++LightId;
50 }
51 CarlaLight->SetId(LightId);
52 }
53 Lights.Add(LightId, CarlaLight);
54 DayTimeChangeEvent.AddUniqueDynamic(CarlaLight, &UCarlaLight::HandleDayTimeChanged);
55 // The day/night state is only broadcast when the weather changes, so a
56 // light that registers later (a World Partition cell streamed in at
57 // night) stayed in its default, day, state until the next change. Applied
58 // on the next tick: from inside registration the lamp blueprint has not
59 // run its BeginPlay yet, and its SetLight looped forever (PIE stopped).
61 {
62 ScheduleDayTimeState(CarlaLight);
63 }
64 }
66}
67
68void UCarlaLightSubsystem::ScheduleDayTimeState(UCarlaLight* CarlaLight)
69{
70 UWorld* World = GetWorld();
71 if (World == nullptr || !World->IsGameWorld())
72 {
73 return;
74 }
75 const bool bFirst = PendingDayTimeLights.Num() == 0;
76 PendingDayTimeLights.Add(CarlaLight);
77 if (!bFirst)
78 {
79 return;
80 }
81 World->GetTimerManager().SetTimerForNextTick(FTimerDelegate::CreateWeakLambda(this, [this]()
82 {
83 TSet<TWeakObjectPtr<UCarlaLight>> Pending = MoveTemp(PendingDayTimeLights);
85 for (const TWeakObjectPtr<UCarlaLight>& Weak : Pending)
86 {
87 UCarlaLight* Light = Weak.Get();
88 // Only lights still registered here: one streamed out meanwhile is skipped.
89 if (IsValid(Light) && Lights.FindKey(Light) != nullptr)
90 {
91 Light->HandleDayTimeChanged(bLastIsDay);
92 Light->ApplyLegacyComponentConversion();
93 }
94 }
96 }));
97}
98
99void UCarlaLightSubsystem::UnregisterLight(UCarlaLight* CarlaLight)
100{
101 if(CarlaLight)
102 {
103 Lights.Remove(CarlaLight->GetId());
104 DayTimeChangeEvent.RemoveDynamic(CarlaLight, &UCarlaLight::HandleDayTimeChanged);
105 }
107}
108
110{
111 // Every weather push notifies (AWeather and the sky rig, twice per
112 // set_weather), but only an actual day/night change needs the broadcast:
113 // it walks every registered light (blueprint handlers, emissive material
114 // instances, intensity conversion) and cost ~270 ms per set_weather on
115 // Town12 with ~4400 lights loaded. Lights registering later get the
116 // current state through ScheduleDayTimeState.
117 // The sky rig notifies too, so the client's set_day_night_cycle(False) is
118 // enforced here rather than only on AWeather.
119 if (!bDayNightCycle || (bHasDayTimeState && bLastIsDay == bIsDay))
120 {
121 return;
122 }
123 bHasDayTimeState = true;
124 bLastIsDay = bIsDay;
125 DayTimeChangeEvent.Broadcast(bIsDay);
126 // Blueprints bind this event too (BlueprintAssignable) and their handlers
127 // push raw authored UE4-era intensities into the light components; a
128 // per-light conversion inside the broadcast gets overwritten by whichever
129 // handler runs later. Convert once the whole broadcast is done.
130 for (auto& LightPair : Lights)
131 {
132 if (UCarlaLight* CarlaLight = LightPair.Value)
133 {
134 CarlaLight->ApplyLegacyComponentConversion();
135 }
136 }
137 // A day/night change flips light states server-side; flag every connected
138 // client so their LightManager re-queries instead of serving stale is_on
139 // values from its local cache.
141}
142
144{
145 for (auto ClientPair : ClientStates)
146 {
147 if(ClientPair.Value)
148 {
149 return true;
150 }
151 }
152 return false;
153}
154
155std::vector<carla::rpc::LightState> UCarlaLightSubsystem::GetLights(FString Client)
156{
157 std::vector<carla::rpc::LightState> result;
158
159 ClientStates.FindOrAdd(Client) = false;
160
161 for(auto& Light : Lights)
162 {
163 UCarlaLight* CarlaLight = Light.Value;
164 // World Partition can stream out a lamp's owner between the light's GC
165 // death and its EndPlay unregistration; this RPC runs on the server
166 // thread and must not touch such carcasses.
167 if (!IsValid(CarlaLight) || !IsValid(CarlaLight->GetOwner()))
168 {
169 continue;
170 }
171 result.push_back(CarlaLight->GetLightState());
172 }
173 return result;
174}
175
177 FString Client,
178 std::vector<carla::rpc::LightState> LightsToSet,
179 bool DiscardClient)
180{
181 bool* ClientState = ClientStates.Find(Client);
182
183 if(ClientState) {
184 for(auto& LightState : LightsToSet) {
185 UCarlaLight* CarlaLight = Lights.FindRef(LightState._id);
186 if(CarlaLight) {
187 CarlaLight->SetLightState(LightState);
188 }
189 }
190 *ClientState = true;
191
192 if(DiscardClient)
193 {
194 ClientStates.Remove(Client);
195 }
196 }
197
198}
199
201{
202 if (Lights.Contains(Id))
203 {
204 return Lights[Id];
205 }
206 return nullptr;
207}
208
210 bDayNightCycle = active;
211 // Re-enabled: the next weather push broadcasts again, even with the same
212 // state, so lights the client changed meanwhile follow the cycle again.
213 if (active)
214 {
215 bHasDayTimeState = false;
216 }
217 TArray<AActor*> WeatherActors;
218 UGameplayStatics::GetAllActorsOfClass(GetWorld(), AWeather::StaticClass(), WeatherActors);
219 if (WeatherActors.Num())
220 {
221 if (AWeather* WeatherActor = Cast<AWeather>(WeatherActors[0]))
222 {
223 WeatherActor->SetDayNightCycle(active);
224 }
225 }
226}
227
228void UCarlaLightSubsystem::SetClientStatesdirty(FString ClientThatUpdate)
229{
230 for(auto& Client : ClientStates)
231 {
232 if(Client.Key != ClientThatUpdate)
233 {
234 Client.Value = true;
235 }
236
237 }
238}
static bool IsValid(const ACarlaWheeledVehicle *Vehicle)
void SetClientStatesdirty(FString ClientThatUpdate)
Flag every registered client (except ClientThatUpdate) so their LightManager re-queries light states.
void Initialize(FSubsystemCollectionBase &Collection) override
TMap< int, UCarlaLight * > Lights
TMap< int, UCarlaLight * > & GetLights()
TMap< FString, bool > ClientStates
void SetLights(FString Client, std::vector< carla::rpc::LightState > LightsToSet, bool DiscardClient=false)
void RegisterLight(UCarlaLight *CarlaLight)
FDayTimeChanged DayTimeChangeEvent
void NotifyDayTimeChange(bool bIsDay)
Broadcast a day/night change and then re-apply the UE4->UE5 component intensity conversion on every r...
void UnregisterLight(UCarlaLight *CarlaLight)
TSet< TWeakObjectPtr< UCarlaLight > > PendingDayTimeLights
void ScheduleDayTimeState(UCarlaLight *CarlaLight)
void SetDayNightCycle(const bool active)
UCarlaLight * GetLight(int Id)